Skip to content
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

Use constant for 180/π in to_degrees #47919

Merged
merged 1 commit into from
Feb 3, 2018

Conversation

varkor
Copy link
Member

@varkor varkor commented Feb 1, 2018

The current f32|f64.to_degrees implementation uses a division to calculate 180/π, which causes a loss of precision. Using a constant is still not perfect (implementing a maximally-precise algorithm would come with a high performance cost), but improves precision with a minimal change.

As per the discussion in #29944, this fixes #29944 (the costs of improving the precision further would not outweigh the gains).

@rust-highfive
Copy link
Collaborator

r? @shepmaster

(rust_highfive has picked a reviewer for you, use r? to override)

@shepmaster
Copy link
Member

r? @rkruppe

you seemed in favor of this.

@@ -239,7 +239,9 @@ impl Float for f32 {
/// Converts to degrees, assuming the number is in radians.
#[inline]
fn to_degrees(self) -> f32 {
self * (180.0f32 / consts::PI)
// Use a constant for better precision.
let pis_in_180 = 57.2957795130823208767981548141051703_f32;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not an actual const?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes more sense stylistically.

@@ -237,7 +237,9 @@ impl Float for f64 {
/// Converts to degrees, assuming the number is in radians.
#[inline]
fn to_degrees(self) -> f64 {
self * (180.0f64 / consts::PI)
// Use a constant for better precision.
let pis_in_180 = 57.2957795130823208767981548141051703_f64;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't there be more digits here than in f32?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both literals actually have more digits than necessary: they both have 36 significant figures, which was chosen to be consistent with the existing definitions of mathematical constants (for both f32 and f64).

@hanna-kruppe
Copy link
Contributor

Curiously, f64::to_degrees already passes the test without this change and 180.0 / f64::consts::PI is exactly equal to the exact value given in #29944 -- which is of course double rounded (since it's a transcendental reduced to a finite decimal string and then converted to a short binary float). I wonder whether that double rounding is significant and we just need a different test case to exhibit the problem, or whether it means we don't need this change for f64::to_degrees at all.

@varkor
Copy link
Member Author

varkor commented Feb 1, 2018

I think in this case, double rounding should not be an issue, because the precision to which the number is rounded in decimal form is significantly higher than the floating-point precision. I imagine the change isn't actually necessary for f64, but probably makes sense for consistency.

@kennytm kennytm added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Feb 1, 2018
@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented Feb 1, 2018

Double rounding can be an issue regardless of what intermediate precision you round to. Suppose the full binary expansion of a number looks like this:

...0100000100...
    ^  ^
    A  B

If we first round (half-to-even) to position B and then to position A, the result ends in 01. If we round to A directly without double rounding, we round up and the result ends in 10. This can happen regardless of how far apart A and B are, it just requires that the bits (A+1)...B are zeros.

@varkor
Copy link
Member Author

varkor commented Feb 1, 2018

If we truncate/round to B and then try flipping the last bit, if there's no difference in the value when rounded to A (before and after flipping the bit), then we can determine the first rounding caused no loss of precision relative to rounding to A, because regardless of the bits following B, the result would still be the same. Am I right in thinking this? This would suggest that the approximation in the value of the f32/f64 constants are as close to the correct value of 180/π as possible, even taking it being transcendental into consideration.

@hanna-kruppe
Copy link
Contributor

I'm not quite sure if I understand correctly, but if you produce anything other than ...0100000000..., you're not rounding to bit B. So whatever you're describing, if it arrives at the correctly rounded result (...1000000000...), then it's not double rounding, i.e., it's not properly rounding to some precision and then afterwards rounding again to some smaller precision.

But this is a theroetical discussion. The actual interesting part is whether the f64 produced from the decimal literal in this code is the correct rounding of the actual value of 180/pi. I'm rather busy now so I don't know when I'll have the time to verify that.
@nagisa, @est31: does one of you have the tools and time to verify?

@varkor
Copy link
Member Author

varkor commented Feb 1, 2018

I guess my real question is that, this code compiles:

fn main() {
    let pis_in_180_below = 57.29577951308232087679815_f64;
    let pis_in_180_above = 57.29577951308232087679816_f64;
    // pis_in_180_below < pis_in_180 < pis_in_180_above (in terms of their literals)
    assert_eq!(pis_in_180_below, pis_in_180_above);
}

so assuming the floating-point literal parser is well-behaved, then any literal value (including a hypothetically-infinite sequence) between these two values is equal to the constant in this code. This assumes that the parser always produces the closest floating-point number to the literal provided (which seems like a reasonable assumption, given that Rust even allows literals with more precision than f64 recognises). However if that is true, then this constant is correct.

@nagisa
Copy link
Member

nagisa commented Feb 1, 2018

In both previous and new code, the division is evaluated during the construction of LLVM-IR and obtains following representations (the hexidecimal form used is whatever the IEEE standard specifies):

  • f32 division = 0x404CA5DC00000000
  • f32 constant = 0x404CA5DC20000000
  • f64 division = 0x404CA5DC1A63C1F8
  • f64 constant = 0x404CA5DC1A63C1F8

So at least for f64, the result is equivalent.

@hanna-kruppe
Copy link
Contributor

@nagisa Thanks! But I believe that just confirms what was observed earlier in this thread? The tricky question is whether you'd also get double 0x404CA5DC1A63C1F8 if you took the transcendental real numner 180/PI and rounded it to f64 precision directly.

@varkor's last comment, however, settles the question with less effort than what I had in mind. I'm convinced now the dedicated constant is both correct and unnecessary for f64.

@@ -237,7 +237,9 @@ impl Float for f64 {
/// Converts to degrees, assuming the number is in radians.
#[inline]
fn to_degrees(self) -> f64 {
self * (180.0f64 / consts::PI)
// Use a constant for better precision.
const PIS_IN_180: f64 = 57.2957795130823208767981548141051703_f64;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned consistency as reason to have the constant here as well but I'd rather not have an opaque magic number duplicated for no functional reason. I think it's better to leave the code in place and add a comment saying that 180.0f64 / consts::PI is correctly rounded.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and probably call out that this differs from f32::to_degrees.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, sure 👍

@varkor varkor force-pushed the to_degrees-precision branch 2 times, most recently from d07c26e to febba80 Compare February 1, 2018 17:48
@hanna-kruppe
Copy link
Contributor

Great, thanks! r=me after squashing

@ranma42
Copy link
Contributor

ranma42 commented Feb 1, 2018

Would it make sense to compute the f64 constant and convert it to f32 in the f32 impl?
This would avoid having a magic number in the code.

The current `f32|f64.to_degrees` implementation uses a division to calculate 180/π, which causes a loss of precision. Using a constant is still not perfect (implementing a maximally-precise algorithm would come with a high performance cost), but improves precision with a minimal change.
@hanna-kruppe
Copy link
Contributor

@ranma42 It's possible that this doesn't give the right result (because it's double rounding differently than the ways examined in previously in this discussion, which turned out to be fine) and it doesn't seem easier to verify that it gives the right result than to verify that the constant is correct.

@hanna-kruppe
Copy link
Contributor

@bors r+

@bors
Copy link
Contributor

bors commented Feb 1, 2018

📌 Commit e34c31b has been approved by rkruppe

@kennytm
Copy link
Member

kennytm commented Feb 1, 2018

@bors rollup

@kennytm kennytm added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Feb 1, 2018
kennytm added a commit to kennytm/rust that referenced this pull request Feb 2, 2018
…ppe Use constant for 180/π in to_degrees The current `f32|f64.to_degrees` implementation uses a division to calculate `180/π`, which causes a loss of precision. Using a constant is still not perfect (implementing a maximally-precise algorithm would come with a high performance cost), but improves precision with a minimal change. As per the discussion in rust-lang#29944, this fixes rust-lang#29944 (the costs of improving the precision further would not outweigh the gains).
bors added a commit that referenced this pull request Feb 3, 2018
Rollup of 10 pull requests

- Successful merges: #46156, #47829, #47842, #47898, #47914, #47916, #47919, #47942, #47951, #47973
- Failed merges: #47753
@bors bors merged commit e34c31b into rust-lang:master Feb 3, 2018
@varkor varkor deleted the to_degrees-precision branch February 3, 2018 15:28
varkor added a commit to varkor/rust that referenced this pull request Feb 28, 2018
rust-lang#47919 regressed some test cases for `f32::to_degrees`, while improving others. This change satisfies all previous test cases and should be more accurate than both previous implementations.

The `f32` to `f64` cast is lossless, `f64::to_degrees` should provide more accuracy than a native `f32::to_degrees`, and conversion back to `f32` will be as accurate a conversion as possible. It can be hard to reason about floating-point accuracy, but given that this passes all the tests, I feel confident it's an improvement.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

to_degrees/to_radians aren't rounded correctly
8 participants