Skip to content

to_degrees/to_radians aren't rounded correctly #29944

Closed
@huonw

Description

@huonw
#![feature(float_extras)]

fn main() {
    let exact = 57.2957795130823208767981548141051703324054724665643215491602438612028471483215526324409689958511109441862233816328648932;

    assert_eq!(1_f32.to_degrees(), exact);
}
thread '<main>' panicked at 'assertion failed: `(left == right)` (left: `57.295776`, right: `57.29578`)', <anon>:6
playpen: application terminated with error code 101

That computed answer is just one bit wrong.

The above case would be addressed by having the 180/pi value used in the conversion being a correctly rounded constant, rather than computed as 180.0 / consts::PI, although I'm not sure this will get the right answer in all cases. If we decide to care about this, then we'll need an exact multiplication algorithm such as Brisebarre and Muller (2008) Correctly rounded multiplication by arbitrary precision constants. Let C be the exact (i.e. infinite precision) value of 180 / pi, then:

// C as f32
const C_H: f32 = 57.295780181884765625;
// (C - C_H) as f32
const C_L: f32 = -6.6880244276035227812826633453369140625e-7;

fn exact_mul(x: f32) -> f32 {
    let a = C_L * x;
    let b = C_H.mul_add(x, a);
    b
}

fn main() {
    let exact = 57.2957795130823208767981548141051703324054724665643215491602438612028471483215526324409689958511109441862233816328648932;
    assert_eq!(exact_mul(1.0), exact);
}

(There's some conditions on the constant C for this to work for all x, and I don't know/haven't yet checked if 180/pi satisfies them.)

This will is noticably slower than the naive method, especially if there's not hardware support for FMA (mul_add), however, if we do implement this, people who don't care about accuracy can use the naive method trivially. That said, to_degrees is almost always used for output for humans, and is often rounded to many fewer decimal places than full precision, so loss of precision doesn't matter... but also, speed probably doesn't matter so much (the formatting will almost certainly be much slower than the multiplication). On the other hand, to_radians won't be used for human output, typically.

#![feature(test, float_extras)]
extern crate test;

// C as f32
const C_H: f32 = 57.295780181884765625;
// (C - C_H) as f32
const C_L: f32 = -6.6880244276035227812826633453369140625e-7;

fn exact_mul(x: f32) -> f32 {
    let a = C_L * x;
    let b = C_H.mul_add(x, a);
    b
}

const FLOATS: &'static [f32] = &[-360.0, -359.99, -100.0, -1.001, 0.001, -1e-40,
                                 0.0,
                                 1e-40, 0.001, 1.001, 100.0, 359.99, 360.0];

#[bench]
fn exact(b: &mut test::Bencher) {
    b.iter(|| {
        for x in test::black_box(FLOATS) {
            test::black_box(exact_mul(*x));
        }
    })
}

#[bench]
fn inexact(b: &mut test::Bencher) {
    b.iter(|| {
        for x in test::black_box(FLOATS) {
            test::black_box(x.to_degrees());
        }
    })
}

#[bench]
fn format(b: &mut test::Bencher) {
    b.iter(|| {
        let mut buf = [0u8; 100];
        for x in test::black_box(FLOATS) {
            use std::io::prelude::*;
            let _ = write!(&mut buf as &mut [_], "{:.0}", x);
            test::black_box(&buf);
        }
    })
}
test exact   ... bench:         169 ns/iter (+/- 6)
test format  ... bench:       1,238 ns/iter (+/- 41)
test inexact ... bench:          84 ns/iter (+/- 3)

(It's likely that to_radians suffers similarly, since pi / 180 is just as transcendental as 180 / pi, but I haven't found an example with a few tests.)


This issue is somewhat of a policy issue: how much do we care about getting correctly rounded floating point answers? This case is pretty simple, but we almost certainly don't want to guarantee 100% precise answers for functions like sin/exp etc., since this is non-trivial/slow to do, and IEEE754 (and typical libm's) don't guarantee exact rounding for all inputs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.P-lowLow priorityT-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions