Closed
Description
use std::time::Duration;
fn main() {
let dur = Duration::from_secs_f64(0.000042);
assert_eq!(dur, Duration::new(0, 42000));
}
The above assertion succeeds on Rust standard library versions prior to 1.60.0. On 1.60.0 it fails with:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `41.999µs`,
right: `42µs`', src/main.rs:6:5
The exact mathematical value represented by 0.000042_f64 is:
0.0000419999999999999976759042230600726952616241760551929473876953125
which is about 41999.999999999998ns. This is astronomically closer to 42000ns than to 41999ns. I believe the correct behavior for from_secs_f64
would be to produce 42000ns as before, not truncate to 41999ns.
Mentioning @newpavlov @nagisa since #90247 seems related and is in the right commit range. The PR summary mentions that the PR would be performing truncation instead of rounding but I don't see that this decision was ever discussed by the library API team. (Mentioning @rust-lang/libs-api to comment on desired behavior.)
Activity
nagisa commentedon Apr 14, 2022
See also #93535 (comment).
dtolnay commentedon Apr 14, 2022
#93535 (comment) says that the decision in favor of truncating was motivated in part because the integer accessors on Duration, such as
subsec_micros
, do truncation not rounding. For example a duration with 41999 nanos would say it has 41 subsec micros, not 42.I don't believe this rationale is correct to apply to
from_secs_f64
. People's expectations are quite different for integer manipulation vs floats. I think it is widely understood that/
performs truncating division for integers and rounding for floats (mandated by IEEE 754).Truncating is especially not a good behavior for
from_secs_f64
because every float that is the closest f64 to an exact number of nanoseconds (like 0.000042_f64 is) is going to be in some tiny tiny interval around that number of nanoseconds, "randomly" either slightly smaller or slightly greater. It would be silly to split that interval and send the bottom half of it to a different result than the top half.(In reality these intervals are 10000000000x smaller than shown in the ASCII art, which makes the truncating behavior even more absurd.)
None of the integer operations on Duration share a similar characteristic to this.
m-ou-se commentedon Apr 14, 2022
Hm,
some_int as f64
rounds, whilesome_float as i64
truncates. This is basically thesome_float as i64
situation, which would suggest that truncating is the right option. Except that integers (whole numbers) are exactly representable, which makes the issue disappear:So, while
0.000042
is slightly less than it should be,(0.000042 * 1e9) as i64
still produces exactly42000
. Interestingly, the rounding that makes this work happens in the multiplication, not in the conversion to i64.m-ou-se commentedon Apr 14, 2022
That's not universal though. For example,
(0.000000015 * 1e9) as i64
is 14, not 15.And that already happened in Duration::from_secs_f64 before 1.60:
Latest Rust:
Rust 1.59.0:
m-ou-se commentedon Apr 14, 2022
It might be reasonable to round to nanoseconds. (Simply add half a nanosecond to the f64 before converting it.) But the previous behaviour was also truncating, not rounding. It's just that rounding errors happen in slightly different places, making this a regression for 0.00042.
m-ou-se commentedon Apr 14, 2022
Testing for every whole number of nanoseconds between 0 and 1, we had 'wrong' answers for 1.7% of them before the recent change (like 0.000000015s = 14ns), and for 48% of them after the change (like 0.00042s = 41999ns).
m-ou-se commentedon Apr 14, 2022
Here's another argument for doing rounding, that does not involve floating point literals: It doesn't round-trip, even if f64 has more than enough precision:
If we simply add 0.5e-9 to the f64 before converting it, to make it round instead of truncate, all these issues disappear.
nagisa commentedon Apr 14, 2022
This is not something we can do. Taking the ASCII art given by dtolnay above, if we were to adopt a round-to-nearest scheme, we'd end up with something like this:
If we were to add half a nanosecond to the floating point value before truncating, we'd end up rounding wrong at the exact points where rounding… “direction” changes (as denoted by
][
).There are some descriptions on how to correctly implement round-to-nearest in software for example here, but it'll naturally make the algorithm that much more complicated.
The roundtrip property does not and cannot hold for the values representable by
Duration
.Duration
can hold 96 bits worth of data.newpavlov commentedon Apr 14, 2022
I've created the experimental PR which implements rounding. I am not 100% sure that rounding is implemented correctly and that I haven't missed any corner cases.
Personally, I think that having truncation in float-duration conversion is fine and that we should prefer it for code simplicity.
17 remaining items