-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Base: correctly rounded floats constructed from rationals #49749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
Regarding performance:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have lots of screen space - please, let's use it not just vertically. We are fine with 92 characters per line (sometimes more if it's just a few characters over) after all.
2a10919 to
50d5aa8
Compare
|
The second commit is the mentioned optimization of using bit twiddling instead of Regarding the 92 character line length limit, does it apply to comments equally? I usually try to have comments be narrower than code, but if you want I'll rewrap them to 92 columns. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for sticking with this! I haven't added the style points to every occurence, but please apply it uniformly.
We can worry about performance afterwards, since this PR got started with correctness. Talking about potential performance improvements without benchmarks is a bit of a moot point.
50d5aa8 to
28d0c8e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
I think the gain in correctness is nice, now let's hope this doesn't cost too much performance 😬
Profiling reveals that a considerable amount of time is spent in the line y = Rational(promote(numerator(x), denominator(x))...)The |
|
Which cases spend their time in promotion in particular? IIRC this should be a noop if there's nothing to pomote/convert 👀 |
The problem is not |
424dc82 to
dd48fdd
Compare
|
Line 329 in dd48fdd
There doesn't seem to be a way around this without introducing a new type for roundings and using it in |
|
This is just for |
|
Is there still something to do here from my side? |
|
Another small optimization would be using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than these, yeah, looks good from my POV 👍
479e877 to
508cfdd
Compare
|
Let me try to take a closer look at this today! Regarding the comment
Indeed most other functions in Base don't allow you to control the rounding, and I don't believe they should! Getting rounding correct is difficult (hence this PR) and it would probably fit better in a separate package. However, Base current does support rounding when converting to |
983c27a to
8fad4a7
Compare
|
Wow! I had no idea the rounding mode argument was already supported for converting rationals to floats! I always assumed the rounding mode argument was only accepted when converting to
|
|
I would also propose to say that this change makes constructing a float from a rational "correctly rounded", instead of "exact". It seems more clear to me at least, since most rationals are not exactly representable as floats. |
8fad4a7 to
314db19
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general I think the change looks good! As far as I can tell the most important methods are to_float_components, to_floating_point_fallback and the to_floating_point_impl implementation for machine floats. The rest is more or less gluing code and tests (both of which there are a lot of).
In general the coding style is quite different from my personal one. That is of course fine, but I have some general comments:
- Often times temporary names are introduced only to be used once. For example the
expvariable into_floating_point_fallback. - There are a lot of
let-blocks that I don't really see the point of. For example into_floating_point_fallbackagain. - There are many type annotations that I don't really see the point of. To take
to_floating_point_fallbackas an example again, surely the compiler can verifyzero(T)::Twithout the type annotation?
| is_zero = num_is_zero & !den_is_zero | ||
| is_inf = !num_is_zero & den_is_zero |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I think it would be natural to make an early return on these checks. So replace them with
num_is_zero & !den_is_zero && return zero(T)
!num_is_zero & den_is_zero && return sb * T(Inf)
num_is_zero & den_is_zero && return T(NaN)That removes the need for the if-statement further down and , which, for me, simplifies the flow.
Similar things could be done in the other implementations, though it makes less of a difference there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to avoid return when possible. It's a jump/goto statement, breaking nice control-flow properties known as structured programming. I do use jumps in control flow, but only when necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used to be a Go nut (as in "Golang aficionado"), so I see where you're coming from, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's only a matter of style of course. I'm not going to fight you over it 😄
| # `BigInt` is a safe default. | ||
| to_float_promote_type(::Type{F}, ::Type{S}) where {F,S} = BigInt | ||
|
|
||
| const BitIntegerOrBool = Union{Bool,Base.BitInteger} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This variable is only used once, I think it is more natural to just use the union directly. Though in this case I guess it does make the method declaration below slightly too long to fit on one line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My reasoning here was basically:
- I'm not exactly sure what's the preferred way to break lines (or format code in general) in Julia source in the Julia project, so I prefer to avoid it completely
- Adding a constant is basically free (as long as it's not huge, doesn't mess with precompilation somehow, etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand, it also explains some of the style of code in other places!
|
I also made a quick benchmark comparing this implementation for With my version I get So 10 times faster for machine size integers and 2 times faster for large integers. Even more if one counts the GC time, which one probably should for I don't think these performance gains are essential though, either version would be fine I think. |
314db19 to
8665610
Compare
Got rid of
Yeah, some of the
There is no way to declare a return type for a function (as in, all the function's methods) in Julia. We don't even have a guarantee for the return type of constructors (#42372)! Thus I use a type assertion after calling some function whenever I can to catch bugs earlier, and to help contain possible type-instability (which can sometimes be introduced by the caller). Note that Julia is good with eliminating type assertions based on type inference, so they're basically free when they're not helpful.
Thanks, I'll profile this and potentially follow up. |
|
Sounds good! I don't have any further comments at this point! |
|
Part of the reason for this PR being slow for bignums is that the current |
|
I think the |
Constructing a floating-point number from a `Rational` should now be correctly rounded. Implementation approach: 1. Convert the (numerator, denominator) pair to a (sign bit, integral significand, exponent) triplet using integer arithmetic. The integer type in question must be wide enough. 2. Convert the above triplet into an instance of the chosen FP type. There is special support for IEEE 754 floating-point and for `BigFloat`, otherwise a fallback using `ldexp` is used. As a bonus, constructing a `BigFloat` from a `Rational` should now be thread-safe when the rounding mode and precision are provided to the constructor, because there is no access to the global precision or rounding mode settings. Updates JuliaLang#45213 Updates JuliaLang#50940 Updates JuliaLang#52507 Fixes JuliaLang#52394 Closes JuliaLang#52395 Fixes JuliaLang#52859
8665610 to
5639ac6
Compare
|
Fixed conflict. Could this be merged as per #53641 (comment) (caused by correctness fix, and the performance regression mainly applies to bignums)? The performance for bignums can be fixed later, this PR is already quite large |
|
The test failures are real. |
|
Is this PR still desired? If so, @nsajko can you rebase and fix the merge conflicts? |
|
Looks like the conflicts are all superficial (just deciding whether to place the new functions before or after other new functions), and this seems already approved so you can add merge me tag after rebasing? |
|
I do see that @LilithHafner previously requested changes, so I think it'd be good to get a re-review to make sure those requests were addressed. |
Constructing a floating-point number from a
Rationalshould now be correctly rounded.Implementation approach:
Convert the (numerator, denominator) pair to a (sign bit, integral significand, exponent) triplet using integer arithmetic. The integer type in question must be wide enough.
Convert the above triplet into an instance of the chosen FP type. There is special support for IEEE 754 floating-point and for
BigFloat, otherwise a fallback usingldexpis used.As a bonus, constructing a
BigFloatfrom aRationalshould now be thread-safe when the rounding mode and precision are provided to the constructor, because there is no access to the global precision or rounding mode settings.Updates #45213
Updates #50940
Updates #52507
Fixes #52394
Closes #52395
Fixes #52859