Skip to content

Commit da6a534

Browse files
committed
Improve precision of Duration-float operations
Rather than convert Duration to a float in order to multiply or divide by a floating point number, which entails a loss of precision, we instead represent both operands as u128 nanoseconds and perform integer arithmetic thereupon.
1 parent 1b9ae9e commit da6a534

2 files changed

Lines changed: 236 additions & 6 deletions

File tree

library/core/src/time.rs

Lines changed: 169 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,79 @@ impl Duration {
10201020
without modifying the original"]
10211021
#[inline]
10221022
pub fn mul_f64(self, rhs: f64) -> Duration {
1023-
Duration::from_secs_f64(rhs * self.as_secs_f64())
1023+
const DURATION_BITS: u32 = 64 + Duration::SECOND.as_nanos().ilog2() + 1;
1024+
1025+
const NUM_BITS_MANT: u32 = f64::MANTISSA_DIGITS - 1;
1026+
const NUM_BITS_EXP: u32 = 64 - f64::MANTISSA_DIGITS;
1027+
1028+
const SIGN_BIT: u64 = 1 << 63;
1029+
const MANT_IMPLIED_BIT: u64 = 1 << NUM_BITS_MANT;
1030+
const MANT_MASK: u64 = MANT_IMPLIED_BIT - 1;
1031+
const EXP_MASK: u64 = (1 << NUM_BITS_EXP) - 1;
1032+
const EXP_BIAS: i32 = 1 - (1 << NUM_BITS_EXP) / 2;
1033+
1034+
let rhs_raw = rhs.to_bits();
1035+
let mant_raw = rhs_raw & MANT_MASK;
1036+
let exp_raw = (rhs_raw >> NUM_BITS_MANT) & EXP_MASK;
1037+
1038+
if exp_raw == EXP_MASK {
1039+
panic!("result is not finite");
1040+
}
1041+
1042+
if self.is_zero() || rhs_raw & !SIGN_BIT == 0 {
1043+
// At least one factor is zero, so their product must be zero.
1044+
return Duration::ZERO;
1045+
}
1046+
1047+
let (mant, biased_exp) = if exp_raw == 0 {
1048+
// `rhs` is subnormal. Renormalize by shifting so that the highest set bit is
1049+
// in the correct leading bit position.
1050+
let shift = mant_raw.leading_zeros() - NUM_BITS_EXP;
1051+
(mant_raw << shift, 1 - shift as i32)
1052+
} else {
1053+
// `rhs` is normal. Set the implied leading bit.
1054+
(mant_raw | MANT_IMPLIED_BIT, exp_raw as i32)
1055+
};
1056+
1057+
// From this point on, consider `mant` to be an integer of `f64::MANTISSA_DIGITS` bits,
1058+
// rather than a normalized fraction; therefore adjust the exponent by `NUM_BITS_MANT`.
1059+
let exp = biased_exp + EXP_BIAS - NUM_BITS_MANT as i32;
1060+
let mant = mant as u128;
1061+
1062+
let lhs_nanos = self.as_nanos();
1063+
1064+
let nanos = if let Ok(exp) = u32::try_from(exp) {
1065+
// `exp` is non-negative, therefore `rhs` has no fractional part. Mere integer
1066+
// multiplication is sufficient.
1067+
mant.checked_shl(exp).and_then(|rhs_int| lhs_nanos.checked_mul(rhs_int))
1068+
} else {
1069+
let nexp = -exp as u32;
1070+
if nexp > DURATION_BITS + f64::MANTISSA_DIGITS {
1071+
// `rhs` is such a small factor that it scales down any `Duration` to less than 0.5ns.
1072+
return Duration::ZERO;
1073+
}
1074+
1075+
let (nexp_, start) = if nexp < 128 { (nexp, 0) } else { (nexp - 128, 1) };
1076+
1077+
// Shift the mantissa across three values, in preparation for long multiplication.
1078+
let mut split = [0; 3];
1079+
split[start..start + 2].copy_from_slice(&[mant >> nexp_, mant << (128 - nexp_)]);
1080+
1081+
// Perform the long multiplication.
1082+
lhs_nanos.checked_mul(split[0]).and_then(|product| {
1083+
let (_, carry) = lhs_nanos.carrying_mul(split[2], 0);
1084+
let (frac, carry) = lhs_nanos.carrying_mul(split[1], carry);
1085+
let round_up = frac >> 127;
1086+
product.checked_add(carry + round_up)
1087+
})
1088+
}
1089+
.expect("result overflows `Duration`");
1090+
1091+
if nanos != 0 && rhs.is_sign_negative() {
1092+
panic!("result is negative")
1093+
}
1094+
1095+
Duration::from_nanos_u128(nanos)
10241096
}
10251097

10261098
/// Multiplies `Duration` by `f32`.
@@ -1033,15 +1105,15 @@ impl Duration {
10331105
/// use std::time::Duration;
10341106
///
10351107
/// let dur = Duration::new(2, 700_000_000);
1036-
/// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_641));
1108+
/// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_283));
10371109
/// assert_eq!(dur.mul_f32(3.14e5), Duration::new(847_800, 0));
10381110
/// ```
10391111
#[stable(feature = "duration_float", since = "1.38.0")]
10401112
#[must_use = "this returns the result of the operation, \
10411113
without modifying the original"]
10421114
#[inline]
10431115
pub fn mul_f32(self, rhs: f32) -> Duration {
1044-
Duration::from_secs_f32(rhs * self.as_secs_f32())
1116+
self.mul_f64(rhs.into())
10451117
}
10461118

10471119
/// Divides `Duration` by `f64`.
@@ -1062,7 +1134,98 @@ impl Duration {
10621134
without modifying the original"]
10631135
#[inline]
10641136
pub fn div_f64(self, rhs: f64) -> Duration {
1065-
Duration::from_secs_f64(self.as_secs_f64() / rhs)
1137+
const DURATION_BITS: u32 = 64 + Duration::SECOND.as_nanos().ilog2() + 1;
1138+
1139+
const NUM_BITS_MANT: u32 = f64::MANTISSA_DIGITS - 1;
1140+
const NUM_BITS_EXP: u32 = 64 - f64::MANTISSA_DIGITS;
1141+
1142+
const SIGN_BIT: u64 = 1 << 63;
1143+
const MANT_IMPLIED_BIT: u64 = 1 << NUM_BITS_MANT;
1144+
const MANT_MASK: u64 = MANT_IMPLIED_BIT - 1;
1145+
const EXP_MASK: u64 = (1 << NUM_BITS_EXP) - 1;
1146+
const EXP_BIAS: i32 = 1 - (1 << NUM_BITS_EXP) / 2;
1147+
1148+
let rhs_raw = rhs.to_bits();
1149+
1150+
if rhs.is_nan() || rhs_raw & !SIGN_BIT == 0 {
1151+
// `rhs` is either NaN or zero.
1152+
panic!("result is not finite");
1153+
}
1154+
1155+
let mant_raw = rhs_raw & MANT_MASK;
1156+
let exp_raw = (rhs_raw >> NUM_BITS_MANT) & EXP_MASK;
1157+
1158+
if self.is_zero() || exp_raw == EXP_MASK {
1159+
// Dividend is zero and/or divisor is infinite.
1160+
return Duration::ZERO;
1161+
}
1162+
1163+
let (mant, biased_exp) = if exp_raw == 0 {
1164+
// `rhs` is subnormal. Renormalize by shifting so that the highest set bit is
1165+
// in the correct leading bit position.
1166+
let shift = mant_raw.leading_zeros() - NUM_BITS_EXP;
1167+
(mant_raw << shift, 1 - shift as i32)
1168+
} else {
1169+
// `rhs` is normal. Set the implied leading bit.
1170+
(mant_raw | MANT_IMPLIED_BIT, exp_raw as i32)
1171+
};
1172+
1173+
// From this point on, consider `mant` to be an integer of `f64::MANTISSA_DIGITS` bits,
1174+
// rather than a normalized fraction; therefore adjust the exponent by `NUM_BITS_MANT`.
1175+
let exp = biased_exp + EXP_BIAS - NUM_BITS_MANT as i32;
1176+
let mant = mant as u128;
1177+
1178+
let lhs_nanos = self.as_nanos();
1179+
1180+
let nanos = if let Ok(exp) = u32::try_from(exp) {
1181+
// `exp` is non-negative, therefore `rhs` has no fractional part. Mere integer
1182+
// division is sufficient.
1183+
(lhs_nanos / mant).unbounded_shr(exp)
1184+
} else {
1185+
// `exp` is negative, therefore `rhs` has both integer and fractional parts.
1186+
1187+
// Calculate the first 127 bits of the reciprocal, and any remainder.
1188+
const MAX: u128 = 1 << 127;
1189+
let recip_q = MAX / mant;
1190+
let recip_r = MAX % mant;
1191+
1192+
// Calculate the next 127 bits of the reciprocal, and shift them into the most
1193+
// significant bits of a u128. Whilst the least significant bit may be incorrect, it is
1194+
// immaterial to the following calculation (it never funnels into `split`).
1195+
let recip_f = ((recip_q * recip_r) + (recip_r * recip_r) / mant) << 1;
1196+
1197+
// Offset exponent by 1 because reciprocal is calculated as 2^127/mant,
1198+
// rather than 2^128/mant (which obviously requires greater width than u128).
1199+
let nexp = (1 - exp) as u32;
1200+
if nexp > DURATION_BITS + f64::MANTISSA_DIGITS {
1201+
// `rhs` is such a small divisor that it scales up any `Duration` to overflow.
1202+
panic!("result overflows `Duration`");
1203+
}
1204+
1205+
let (nexp_, start) = if nexp < 128 { (nexp, 1) } else { (nexp - 128, 0) };
1206+
1207+
// Shift the reciprocal across three values, in preparation for long multiplication.
1208+
let mut split = [0; 3];
1209+
split[start..start + 2]
1210+
.copy_from_slice(&[recip_q >> (128 - nexp_), recip_q.funnel_shl(recip_f, nexp_)]);
1211+
1212+
// Perform the long multiplication.
1213+
lhs_nanos
1214+
.checked_mul(split[1])
1215+
.and_then(|product| {
1216+
let (_, carry) = lhs_nanos.carrying_mul(split[0], 0);
1217+
let (frac, carry) = lhs_nanos.carrying_mul(split[2], carry);
1218+
let round_up = frac >> 127;
1219+
product.checked_add(carry + round_up)
1220+
})
1221+
.expect("result overflows `Duration`")
1222+
};
1223+
1224+
if nanos != 0 && rhs.is_sign_negative() {
1225+
panic!("result is negative")
1226+
}
1227+
1228+
Duration::from_nanos_u128(nanos)
10661229
}
10671230

10681231
/// Divides `Duration` by `f32`.
@@ -1077,15 +1240,15 @@ impl Duration {
10771240
/// let dur = Duration::new(2, 700_000_000);
10781241
/// // note that due to rounding errors result is slightly
10791242
/// // different from 0.859_872_611
1080-
/// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_580));
1243+
/// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_583));
10811244
/// assert_eq!(dur.div_f32(3.14e5), Duration::new(0, 8_599));
10821245
/// ```
10831246
#[stable(feature = "duration_float", since = "1.38.0")]
10841247
#[must_use = "this returns the result of the operation, \
10851248
without modifying the original"]
10861249
#[inline]
10871250
pub fn div_f32(self, rhs: f32) -> Duration {
1088-
Duration::from_secs_f32(self.as_secs_f32() / rhs)
1251+
self.div_f64(rhs.into())
10891252
}
10901253

10911254
/// Divides `Duration` by `Duration` and returns `f64`.

library/coretests/tests/time.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,3 +598,70 @@ fn from_neg_zero() {
598598
assert_eq!(Duration::from_secs_f32(-0.0), Duration::ZERO);
599599
assert_eq!(Duration::from_secs_f64(-0.0), Duration::ZERO);
600600
}
601+
602+
#[test]
603+
fn precise_duration_fp_mul() {
604+
let d1 = Duration::from_nanos_u128(1 << 90);
605+
let d2 = Duration::from_nanos_u128(2 << 90);
606+
let d3 = Duration::from_nanos_u128(3 << 90);
607+
608+
assert_eq!(d1.mul_f32(1.0), d1);
609+
assert_eq!(d1.mul_f32(2.0), d2);
610+
assert_eq!(d1.mul_f32(3.0), d3);
611+
assert_eq!(d2.mul_f32(1.5), d3);
612+
613+
let _ = Duration::MAX.mul_f32(1.0);
614+
}
615+
616+
#[test]
617+
fn precise_duration_fp_div() {
618+
let d1 = Duration::from_nanos_u128(1 << 90);
619+
let d2 = Duration::from_nanos_u128(2 << 90);
620+
let d3 = Duration::from_nanos_u128(3 << 90);
621+
622+
assert_eq!(d1.div_f32(1.0), d1);
623+
assert_eq!(d2.div_f32(2.0), d1);
624+
assert_eq!(d3.div_f32(3.0), d1);
625+
assert_eq!(d3.div_f32(1.5), d2);
626+
627+
let _ = Duration::MAX.div_f32(1.0);
628+
}
629+
630+
const TOO_LARGE: f64 = Duration::MAX.as_nanos() as f64;
631+
632+
// Smallest factor is half the smallest divisor due to rounding in the opposite direction.
633+
const SMALLEST_DIVISOR: f64 = TOO_LARGE.recip();
634+
const SMALLEST_FACTOR: f64 = SMALLEST_DIVISOR / 2.0;
635+
636+
#[test]
637+
fn precise_duration_fp_boundaries() {
638+
const DURATION_BITS: u32 = 64 + Duration::SECOND.as_nanos().ilog2() + 1;
639+
const PRECISION: u32 = DURATION_BITS - f64::MANTISSA_DIGITS;
640+
641+
assert_eq!(Duration::MAX.mul_f64(SMALLEST_FACTOR), Duration::NANOSECOND);
642+
assert_eq!(Duration::MAX.mul_f64(SMALLEST_FACTOR.next_down()), Duration::ZERO);
643+
assert_eq!(Duration::MAX.div_f64(TOO_LARGE), Duration::ZERO);
644+
assert_eq!(Duration::MAX.div_f64(TOO_LARGE.next_down()), Duration::NANOSECOND);
645+
646+
// the following assertions pair with the two subsequent (`should_panic`) tests
647+
assert!(
648+
Duration::MAX - Duration::NANOSECOND.mul_f64(TOO_LARGE.next_down())
649+
< Duration::from_nanos(1 << PRECISION)
650+
);
651+
assert!(
652+
Duration::MAX - Duration::NANOSECOND.div_f64(SMALLEST_DIVISOR)
653+
< Duration::from_nanos(1 << PRECISION)
654+
);
655+
}
656+
657+
#[test]
658+
#[should_panic(expected = "overflow")]
659+
fn precise_duration_fp_mul_overflow() {
660+
let _ = Duration::NANOSECOND.mul_f64(TOO_LARGE);
661+
}
662+
663+
#[test]
664+
#[should_panic(expected = "overflow")]
665+
fn precise_duration_fp_div_overflow() {
666+
let _ = Duration::NANOSECOND.div_f64(SMALLEST_DIVISOR.next_down());
667+
}

0 commit comments

Comments
 (0)