-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Regression in rounding of Duration::from_secs_f64 in 1.60.0 #96045
Comments
See also #93535 (comment). |
#93535 (comment) says that the decision in favor of truncating was motivated in part because the integer accessors on Duration, such as I don't believe this rationale is correct to apply to Truncating is especially not a good behavior for
(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. |
Hm,
So, while |
That's not universal though. For example, And that already happened in Duration::from_secs_f64 before 1.60: Latest Rust:
Rust 1.59.0:
|
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. |
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). |
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. |
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 |
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. |
As @nagisa wrote, the notion of round-trip you are using (duration -> f64 -> duration) is not something that can be lossless in general, so this doesn't work as an argument against truncating. However there is a different round trip that would be valuable to support but broken by truncation: f64 -> duration -> f64 -> duration. Or in practical terms:
The exact string representation of the float in the original incoming input is obviously not preserved because there could've been arbitrarily many irrelevant digits there, but a desirable property is that the first Duration and the second Duration have the same value. Truncation breaks this because if 2 consecutive nanosecond quantities both truncate, then each time you deserialize+serialize, your data is gonna decrease by 1 nanosecond, potentially several times in the course of processing this request/whatever. use std::time::Duration;
fn main() {
let config_file = "0.00004205";
let f = config_file.parse::<f64>().unwrap();
let first_duration = Duration::from_secs_f64(f);
let config_file = first_duration.as_secs_f64().to_string(); // 0.000042049
let f = config_file.parse::<f64>().unwrap();
let second_duration = Duration::from_secs_f64(f);
assert_eq!(first_duration, second_duration); // desirable property
} thread 'main' panicked at 'assertion failed: `(left == right)`
left: `42.049µs`,
right: `42.048µs`', src/main.rs:11:5 |
For me, ultimately I don't care so much about any of the "round trip" properties as @m-ou-se brought up. Whatever you end up doing with these duration values, The use case I do care about is logging. For example we have a config file containing |
Even with rounding such cases are absolutely possible. For example, with both rounding and truncation 0.7s represented as |
I understand that f32 does not have 9 complete digits of sub-second precision, and neither does f64 for durations longer than tens of millions of seconds. But f64 has way more than enough precision that we shouldn't be making these inaccuracies for all the ordinary durations that get used in the real world. f64 has enough precision that you can uniquely represent every duration up to 97 days to nanosecond precision. We really should not be messing this up in |
Wouldn't it be better for cases like this to introduce a |
That's possible but I wouldn't consider it better than rounding in |
Popping up a level here, given that there was a change in behavior that wasn't explicitly discussed by libs-api, does it make sense to revert first and then discuss later whether the change should be made? I understand it might be expedient to just try and have the discussion now, but if there's no immediately obvious consensus, then it might make sense to revert this behavior for now. (Unless I'm missing something and this behavior change was tied to something else that makes reverting difficult?) |
@BurntSushi
BTW, you've used a wrong character, the correct one is µ, i.e. U+B5, not U+03BC in the Greek and Coptic block. It's one of the "fun" Unicode peculiarities. |
This seems like an extremely compelling argument. |
We discussed this in today's @rust-lang/libs meeting, and while we don't think this is anywhere near serious enough to warrant a stable backport, it's worth fixing to round again. |
Not that it's particularly important, this is not really accurate. That is, the character does exist, but according to https://www.unicode.org/reports/tr25/ U+03BC should be preferred, and the dedicated micro character is mostly present for compatibility:
|
@thomcc In this context, compatibility and wider symbol availability would be a good thing. But I think that's off-topic for this issue. |
The above assertion succeeds on Rust standard library versions prior to 1.60.0. On 1.60.0 it fails with:
The exact mathematical value represented by 0.000042_f64 is:
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.)
The text was updated successfully, but these errors were encountered: