From 28a6bcfe7b307476a03f99459c066c263e0bf55d Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Thu, 19 Mar 2026 10:21:54 -0700 Subject: [PATCH 1/4] Use `f128` internally for `Duration`-float methods If we want to keep as much `Duration` precision as possible when multiplying or dividing by a float, then we shouldn't convert it to `f32` nor `f64` to match the operand, because their mantissas aren't large enough to hold a full `Duration`. However, `f128` is large enough with `MANTISSA_DIGITS = 113`, since `Duration::MAX` only needs 94 bits. --- library/core/src/time.rs | 28 +++++++++++++++++++------ library/coretests/tests/time.rs | 36 +++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/library/core/src/time.rs b/library/core/src/time.rs index a5b654033ba14..1536a13df294b 100644 --- a/library/core/src/time.rs +++ b/library/core/src/time.rs @@ -1000,6 +1000,18 @@ impl Duration { } } + #[inline] + #[track_caller] + fn from_nanos_f128(nanos: f128) -> Duration { + if nanos < 0.0 { + panic!("{}", TryFromFloatSecsError { kind: TryFromFloatSecsErrorKind::Negative }); + } else if nanos <= const { Self::MAX.as_nanos() as f128 } { + Self::from_nanos_u128(nanos.round_ties_even() as u128) + } else { + panic!("{}", TryFromFloatSecsError { kind: TryFromFloatSecsErrorKind::OverflowOrNan }); + } + } + /// Multiplies `Duration` by `f64`. /// /// # Panics @@ -1018,7 +1030,8 @@ impl Duration { without modifying the original"] #[inline] pub fn mul_f64(self, rhs: f64) -> Duration { - Duration::from_secs_f64(rhs * self.as_secs_f64()) + let nanos = (self.as_nanos() as f128) * (rhs as f128); + Self::from_nanos_f128(nanos) } /// Multiplies `Duration` by `f32`. @@ -1031,7 +1044,7 @@ impl Duration { /// use std::time::Duration; /// /// let dur = Duration::new(2, 700_000_000); - /// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_641)); + /// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_283)); /// assert_eq!(dur.mul_f32(3.14e5), Duration::new(847_800, 0)); /// ``` #[stable(feature = "duration_float", since = "1.38.0")] @@ -1039,7 +1052,8 @@ impl Duration { without modifying the original"] #[inline] pub fn mul_f32(self, rhs: f32) -> Duration { - Duration::from_secs_f32(rhs * self.as_secs_f32()) + let nanos = (self.as_nanos() as f128) * (rhs as f128); + Self::from_nanos_f128(nanos) } /// Divides `Duration` by `f64`. @@ -1060,7 +1074,8 @@ impl Duration { without modifying the original"] #[inline] pub fn div_f64(self, rhs: f64) -> Duration { - Duration::from_secs_f64(self.as_secs_f64() / rhs) + let nanos = (self.as_nanos() as f128) / (rhs as f128); + Self::from_nanos_f128(nanos) } /// Divides `Duration` by `f32`. @@ -1075,7 +1090,7 @@ impl Duration { /// let dur = Duration::new(2, 700_000_000); /// // note that due to rounding errors result is slightly /// // different from 0.859_872_611 - /// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_580)); + /// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_583)); /// assert_eq!(dur.div_f32(3.14e5), Duration::new(0, 8_599)); /// ``` #[stable(feature = "duration_float", since = "1.38.0")] @@ -1083,7 +1098,8 @@ impl Duration { without modifying the original"] #[inline] pub fn div_f32(self, rhs: f32) -> Duration { - Duration::from_secs_f32(self.as_secs_f32() / rhs) + let nanos = (self.as_nanos() as f128) / (rhs as f128); + Self::from_nanos_f128(nanos) } /// Divides `Duration` by `Duration` and returns `f64`. diff --git a/library/coretests/tests/time.rs b/library/coretests/tests/time.rs index 5877f662b7ddc..59a58fd96ad48 100644 --- a/library/coretests/tests/time.rs +++ b/library/coretests/tests/time.rs @@ -642,15 +642,15 @@ fn duration_fp_div_negative() { } const TOO_LARGE_FACTOR: f64 = Duration::MAX.as_nanos() as f64; -const TOO_LARGE_DIVISOR: f64 = (Duration::MAX.as_secs_f64() * 2e9).next_up(); -const SMALLEST_DIVISOR: f64 = (TOO_LARGE_DIVISOR.recip() * 2.0).next_up().next_up(); +const TOO_LARGE_DIVISOR: f64 = TOO_LARGE_FACTOR * 2.0; +const SMALLEST_DIVISOR: f64 = TOO_LARGE_DIVISOR.recip() * 2.0; const SMALLEST_FACTOR: f64 = TOO_LARGE_FACTOR.recip() / 2.0; -const SMALLEST_NEGFACTOR: f64 = (0.0f64.next_down() * 0.5e9).next_up(); +const SMALLEST_NEGFACTOR: f64 = -0.0f64; #[test] fn duration_fp_boundaries() { const DURATION_BITS: u32 = Duration::MAX.as_nanos().ilog2() + 1; - const PRECISION: u32 = DURATION_BITS - f64::MANTISSA_DIGITS + 1; + const PRECISION: u32 = DURATION_BITS - f64::MANTISSA_DIGITS; assert_eq!(Duration::MAX.mul_f64(0.0), Duration::ZERO); assert_eq!(Duration::MAX.mul_f64(-0.0), Duration::ZERO); @@ -695,3 +695,31 @@ fn duration_fp_mul_overflow() { fn duration_fp_div_overflow() { let _ = Duration::NANOSECOND.div_f64(SMALLEST_DIVISOR.next_down()); } + +#[test] +fn precise_duration_fp_mul() { + let d1 = Duration::from_nanos_u128(1 << 90); + let d2 = Duration::from_nanos_u128(2 << 90); + let d3 = Duration::from_nanos_u128(3 << 90); + + assert_eq!(d1.mul_f32(1.0), d1); + assert_eq!(d1.mul_f32(2.0), d2); + assert_eq!(d1.mul_f32(3.0), d3); + assert_eq!(d2.mul_f32(1.5), d3); + + let _ = Duration::MAX.mul_f32(1.0); +} + +#[test] +fn precise_duration_fp_div() { + let d1 = Duration::from_nanos_u128(1 << 90); + let d2 = Duration::from_nanos_u128(2 << 90); + let d3 = Duration::from_nanos_u128(3 << 90); + + assert_eq!(d1.div_f32(1.0), d1); + assert_eq!(d2.div_f32(2.0), d1); + assert_eq!(d3.div_f32(3.0), d1); + assert_eq!(d3.div_f32(1.5), d2); + + let _ = Duration::MAX.div_f32(1.0); +} From 18d671e9ec5b33e9544af4b8b27c3fb64c2c35b4 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Fri, 20 Mar 2026 16:55:23 -0700 Subject: [PATCH 2/4] Improve Duration-float rounding by using subnormals --- library/core/src/time.rs | 49 +++++++++++++++++++++++++-------- library/coretests/tests/time.rs | 13 +++++++++ 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/library/core/src/time.rs b/library/core/src/time.rs index 1536a13df294b..300cd46e87229 100644 --- a/library/core/src/time.rs +++ b/library/core/src/time.rs @@ -1002,14 +1002,43 @@ impl Duration { #[inline] #[track_caller] - fn from_nanos_f128(nanos: f128) -> Duration { + fn from_nanos_f128_subnormal(nanos: f128) -> Duration { + // NB: the mul/div methods below are converting nanos with `f128::from_bits`, which puts + // them in the subnormal range, with the value packed in the least-significant bits of the + // mantissa -- even `Duration::MAX` fits this way. This avoids double-rounding operations, + // since we're not keeping any excess precision, and we can convert back `to_bits()`. if nanos < 0.0 { panic!("{}", TryFromFloatSecsError { kind: TryFromFloatSecsErrorKind::Negative }); - } else if nanos <= const { Self::MAX.as_nanos() as f128 } { - Self::from_nanos_u128(nanos.round_ties_even() as u128) - } else { + } else if nanos == 0.0 { + // In particular, -0.0 is an exception that we can't just + // convert `to_bits()`, but it's still a valid zero. + return Duration::ZERO; + } + let nanos = nanos.to_bits(); + if nanos > const { Self::MAX.as_nanos() } { panic!("{}", TryFromFloatSecsError { kind: TryFromFloatSecsErrorKind::OverflowOrNan }); } + Self::from_nanos_u128(nanos) + } + + #[inline] + #[track_caller] + fn mul_f128(self, rhs: f128) -> Duration { + if rhs < 0.0 && self != Self::ZERO { + panic!("{}", TryFromFloatSecsError { kind: TryFromFloatSecsErrorKind::Negative }); + } + let nanos = f128::from_bits(self.as_nanos()) * (rhs as f128); + Self::from_nanos_f128_subnormal(nanos) + } + + #[inline] + #[track_caller] + fn div_f128(self, rhs: f128) -> Duration { + if rhs < 0.0 && rhs.is_finite() && self != Self::ZERO { + panic!("{}", TryFromFloatSecsError { kind: TryFromFloatSecsErrorKind::Negative }); + } + let nanos = f128::from_bits(self.as_nanos()) / (rhs as f128); + Self::from_nanos_f128_subnormal(nanos) } /// Multiplies `Duration` by `f64`. @@ -1030,8 +1059,7 @@ impl Duration { without modifying the original"] #[inline] pub fn mul_f64(self, rhs: f64) -> Duration { - let nanos = (self.as_nanos() as f128) * (rhs as f128); - Self::from_nanos_f128(nanos) + self.mul_f128(rhs.into()) } /// Multiplies `Duration` by `f32`. @@ -1052,8 +1080,7 @@ impl Duration { without modifying the original"] #[inline] pub fn mul_f32(self, rhs: f32) -> Duration { - let nanos = (self.as_nanos() as f128) * (rhs as f128); - Self::from_nanos_f128(nanos) + self.mul_f128(rhs.into()) } /// Divides `Duration` by `f64`. @@ -1074,8 +1101,7 @@ impl Duration { without modifying the original"] #[inline] pub fn div_f64(self, rhs: f64) -> Duration { - let nanos = (self.as_nanos() as f128) / (rhs as f128); - Self::from_nanos_f128(nanos) + self.div_f128(rhs.into()) } /// Divides `Duration` by `f32`. @@ -1098,8 +1124,7 @@ impl Duration { without modifying the original"] #[inline] pub fn div_f32(self, rhs: f32) -> Duration { - let nanos = (self.as_nanos() as f128) / (rhs as f128); - Self::from_nanos_f128(nanos) + self.div_f128(rhs.into()) } /// Divides `Duration` by `Duration` and returns `f64`. diff --git a/library/coretests/tests/time.rs b/library/coretests/tests/time.rs index 59a58fd96ad48..8ae7a2e9ae6b7 100644 --- a/library/coretests/tests/time.rs +++ b/library/coretests/tests/time.rs @@ -723,3 +723,16 @@ fn precise_duration_fp_div() { let _ = Duration::MAX.div_f32(1.0); } + +#[test] +fn duration_fp_mul_rounding() { + // This precise result in ns would start 9223372036854777855999999999.4999999999999998... + // If that is rounded too early to 9223372036854777855999999999.5, + // then the final result would be incorrectly rounded up again. + assert_eq!( + Duration::MAX.mul_f64(0.5_f64.next_up()), + Duration::from_nanos_u128(9223372036854777855999999999) + ); + // This is precisely 9223372036854775807999999999.5 ns, which *should* round up. + assert_eq!(Duration::MAX.mul_f64(0.5_f64), Duration::from_secs(9223372036854775808)); +} From fd3b09aaa8646b6731c1e4812b4e228df1f5b658 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 21 Mar 2026 11:06:06 -0700 Subject: [PATCH 3/4] Heed `target_has_reliable_f128` in Duration-float ops --- library/core/src/time.rs | 23 +++++++++++++++++++---- library/coretests/tests/time.rs | 21 +++++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/library/core/src/time.rs b/library/core/src/time.rs index 300cd46e87229..57166b09e5b2c 100644 --- a/library/core/src/time.rs +++ b/library/core/src/time.rs @@ -1002,6 +1002,7 @@ impl Duration { #[inline] #[track_caller] + #[cfg(target_has_reliable_f128)] fn from_nanos_f128_subnormal(nanos: f128) -> Duration { // NB: the mul/div methods below are converting nanos with `f128::from_bits`, which puts // them in the subnormal range, with the value packed in the least-significant bits of the @@ -1023,6 +1024,7 @@ impl Duration { #[inline] #[track_caller] + #[cfg(target_has_reliable_f128)] fn mul_f128(self, rhs: f128) -> Duration { if rhs < 0.0 && self != Self::ZERO { panic!("{}", TryFromFloatSecsError { kind: TryFromFloatSecsErrorKind::Negative }); @@ -1033,6 +1035,7 @@ impl Duration { #[inline] #[track_caller] + #[cfg(target_has_reliable_f128)] fn div_f128(self, rhs: f128) -> Duration { if rhs < 0.0 && rhs.is_finite() && self != Self::ZERO { panic!("{}", TryFromFloatSecsError { kind: TryFromFloatSecsErrorKind::Negative }); @@ -1059,7 +1062,10 @@ impl Duration { without modifying the original"] #[inline] pub fn mul_f64(self, rhs: f64) -> Duration { - self.mul_f128(rhs.into()) + crate::cfg_select! { + target_has_reliable_f128 => self.mul_f128(rhs.into()), + _ => Duration::from_secs_f64(rhs * self.as_secs_f64()), + } } /// Multiplies `Duration` by `f32`. @@ -1080,7 +1086,10 @@ impl Duration { without modifying the original"] #[inline] pub fn mul_f32(self, rhs: f32) -> Duration { - self.mul_f128(rhs.into()) + crate::cfg_select! { + target_has_reliable_f128 => self.mul_f128(rhs.into()), + _ => self.mul_f64(rhs.into()), + } } /// Divides `Duration` by `f64`. @@ -1101,7 +1110,10 @@ impl Duration { without modifying the original"] #[inline] pub fn div_f64(self, rhs: f64) -> Duration { - self.div_f128(rhs.into()) + crate::cfg_select! { + target_has_reliable_f128 => self.div_f128(rhs.into()), + _ => Duration::from_secs_f64(self.as_secs_f64() / rhs), + } } /// Divides `Duration` by `f32`. @@ -1124,7 +1136,10 @@ impl Duration { without modifying the original"] #[inline] pub fn div_f32(self, rhs: f32) -> Duration { - self.div_f128(rhs.into()) + crate::cfg_select! { + target_has_reliable_f128 => self.div_f128(rhs.into()), + _ => self.div_f64(rhs.into()), + } } /// Divides `Duration` by `Duration` and returns `f64`. diff --git a/library/coretests/tests/time.rs b/library/coretests/tests/time.rs index 8ae7a2e9ae6b7..12f0da4d37d28 100644 --- a/library/coretests/tests/time.rs +++ b/library/coretests/tests/time.rs @@ -642,15 +642,25 @@ fn duration_fp_div_negative() { } const TOO_LARGE_FACTOR: f64 = Duration::MAX.as_nanos() as f64; -const TOO_LARGE_DIVISOR: f64 = TOO_LARGE_FACTOR * 2.0; -const SMALLEST_DIVISOR: f64 = TOO_LARGE_DIVISOR.recip() * 2.0; const SMALLEST_FACTOR: f64 = TOO_LARGE_FACTOR.recip() / 2.0; -const SMALLEST_NEGFACTOR: f64 = -0.0f64; + +cfg_select! { + target_has_reliable_f128 => { + const TOO_LARGE_DIVISOR: f64 = TOO_LARGE_FACTOR * 2.0; + const SMALLEST_DIVISOR: f64 = TOO_LARGE_DIVISOR.recip() * 2.0; + const SMALLEST_NEGFACTOR: f64 = -0.0f64; + } + _ => { + const TOO_LARGE_DIVISOR: f64 = (Duration::MAX.as_secs_f64() * 2e9).next_up(); + const SMALLEST_DIVISOR: f64 = (TOO_LARGE_DIVISOR.recip() * 2.0).next_up().next_up(); + const SMALLEST_NEGFACTOR: f64 = (0.0f64.next_down() * 0.5e9).next_up(); + } +} #[test] fn duration_fp_boundaries() { const DURATION_BITS: u32 = Duration::MAX.as_nanos().ilog2() + 1; - const PRECISION: u32 = DURATION_BITS - f64::MANTISSA_DIGITS; + const PRECISION: u32 = DURATION_BITS - f64::MANTISSA_DIGITS + 1; assert_eq!(Duration::MAX.mul_f64(0.0), Duration::ZERO); assert_eq!(Duration::MAX.mul_f64(-0.0), Duration::ZERO); @@ -697,6 +707,7 @@ fn duration_fp_div_overflow() { } #[test] +#[cfg_attr(not(target_has_reliable_f128), ignore)] fn precise_duration_fp_mul() { let d1 = Duration::from_nanos_u128(1 << 90); let d2 = Duration::from_nanos_u128(2 << 90); @@ -711,6 +722,7 @@ fn precise_duration_fp_mul() { } #[test] +#[cfg_attr(not(target_has_reliable_f128), ignore)] fn precise_duration_fp_div() { let d1 = Duration::from_nanos_u128(1 << 90); let d2 = Duration::from_nanos_u128(2 << 90); @@ -725,6 +737,7 @@ fn precise_duration_fp_div() { } #[test] +#[cfg_attr(not(target_has_reliable_f128), ignore)] fn duration_fp_mul_rounding() { // This precise result in ns would start 9223372036854777855999999999.4999999999999998... // If that is rounded too early to 9223372036854777855999999999.5, From b73a308ba5554460b8bbd0b973afa2be2551c9a6 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Tue, 7 Apr 2026 12:57:08 -0700 Subject: [PATCH 4/4] Add rounding tests for `Duration::div_f64` --- library/coretests/tests/time.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/library/coretests/tests/time.rs b/library/coretests/tests/time.rs index 12f0da4d37d28..dee850b904373 100644 --- a/library/coretests/tests/time.rs +++ b/library/coretests/tests/time.rs @@ -749,3 +749,26 @@ fn duration_fp_mul_rounding() { // This is precisely 9223372036854775807999999999.5 ns, which *should* round up. assert_eq!(Duration::MAX.mul_f64(0.5_f64), Duration::from_secs(9223372036854775808)); } + +#[test] +#[cfg_attr(not(target_has_reliable_f128), ignore)] +fn duration_fp_div_rounding() { + let nanos = 1u128 << 93; + let divisor = ((1u64 << 47) - 1) as f64 / (1u64 << 47) as f64; + // This precise result in ns would start 9903520314283112567937171456.500000000000003... + // If that is rounded too early to 9903520314283112567937171456.5, + // then the final result may be incorrectly rounded down again by + // `round_to_even()`, but `round()` would have been ok in this case. + assert_eq!( + Duration::from_nanos_u128(nanos).div_f64(divisor), + Duration::from_nanos_u128(9903520314283112567937171457), + ); + // This precise result in ns would start 9903520314283112567937171455.499999999999996... + // If that is rounded too early to 9903520314283112567937171455.5, + // then the final result would be incorrectly rounded up again, + // whether that used `round()` or `round_to_even()`. + assert_eq!( + Duration::from_nanos_u128(nanos - 1).div_f64(divisor), + Duration::from_nanos_u128(9903520314283112567937171455), + ); +}