-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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: correctly rounded floats constructed from rationals #49749
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
5544f1a
to
cc79e0c
Compare
Added a test, apart from addressing the comments. |
cc79e0c
to
bb7c3ac
Compare
Expanded the |
bb7c3ac
to
983c27a
Compare
(; integral_significand, exponent) | ||
end | ||
|
||
function to_float_components(::Type{T}, num, den, precision, max_subnormal_exp, romo, sb) where {T} |
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.
does this method need to exist? if this is purely for internal use, surely the caller can be responsible for converting num
to the type they want.
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 also used in mpfr.jl
base/rational.jl
Outdated
(numerator(y), denominator(y)) | ||
end | ||
|
||
function rational_to_floating_point(::Type{F}, x, rm = RoundNearest, prec = precision(F)) where {F} |
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.
do we actually care about giving the user the ability to specify rounding modes here? Most other functions in Base
don't, and giving up that seems like it would simplify the code a lot.
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 need the rounding modes for BigFloat
. Also, having rounding modes other than the default one enables more thorough testing that applies to all rounding modes.
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.
Most of the logic for the rounding modes is now in rounding.jl anyway.
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.
to_floating_point_impl(::Type{T}, ::Type{S}, num, den, rm, prec) where {T<:Base.IEEEFloat,S}
could be simplified by requiring rm == RoundNearest
, but then the tests would suffer.
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.
giving the user the ability
to be clear, that particular method is not meant to be public (I think the PR doesn't add any new public names)
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.
Can't we do the BigFloat
ones by just doing arbitrary precision BigFloat
division? I'm not really sure if it's worth adding a bunch of code to make BigFloat(::Rational)
fast.
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 don't think that would be an improvement, by any metric. The relevant code was already added in #50691 (into rounding.jl), so now it doesn't cost anything to support the known rounding modes for BigFloat
, although the IEEEFloat
method could be simplified a bit if necessary, at the cost of some testing, as mentioned above.
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.
by any metric
Maybe the way you and @Joel-Dahne propose would be faster, though? But still, having all float types under a single code path seems better as any bugs should manifest (and be fixed) sooner, so it's more maintainable.
Oh, some of the tests are failing on 32-bit Linux and 32-bit Windows. EDIT: it's because |
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
exp
variable 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_fallback
again. - There are many type annotations that I don't really see the point of. To take
to_floating_point_fallback
as an example again, surely the compiler can verifyzero(T)::T
without 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. |
Constructing a floating-point number from a
Rational
should 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 usingldexp
is used.As a bonus, constructing a
BigFloat
from aRational
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 #45213
Updates #50940
Updates #52507
Fixes #52394
Closes #52395
Fixes #52859