Skip to content

Commit 05395cb

Browse files
committed
Fix sum() accuracy
`[1e20, 1.0, -1e20].sum()` returns `0.0`. This happens because during the summation, `1.0` is too small relative to `1e20`, making it negligible. I have tried Kahan summation but it hasn't fixed the problem. Therefore, I've used Python's `fsum()` implementation with some help from Jason Fager and Huon Wilson. For more details, read: www.cs.cmu.edu/~quake-papers/robust-arithmetic.ps Moreover, benchmark and unit tests were added. Note: `Status.sum` is still not fully fixed. It doesn't handle NaNs, infinities and overflow correctly. See issue 11059: #11059
1 parent 4e0cb31 commit 05395cb

File tree

1 file changed

+66
-1
lines changed

1 file changed

+66
-1
lines changed

src/libextra/stats.rs

+66-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use std::cmp;
1515
use std::hashmap;
1616
use std::io;
1717
use std::num;
18+
use std::util;
1819

1920
// NB: this can probably be rewritten in terms of num::Num
2021
// to be less f64-specific.
@@ -23,6 +24,12 @@ use std::num;
2324
pub trait Stats {
2425

2526
/// Sum of the samples.
27+
///
28+
/// Note: this method sacrifices performance at the altar of accuracy
29+
/// Depends on IEEE-754 arithmetic guarantees. See proof of correctness at:
30+
/// ["Adaptive Precision Floating-Point Arithmetic and Fast Robust Geometric Predicates"]
31+
/// (http://www.cs.cmu.edu/~quake-papers/robust-arithmetic.ps)
32+
/// *Discrete & Computational Geometry 18*, 3 (Oct 1997), 305-363, Shewchuk J.R.
2633
fn sum(self) -> f64;
2734

2835
/// Minimum value of the samples.
@@ -147,8 +154,37 @@ impl Summary {
147154

148155
impl<'self> Stats for &'self [f64] {
149156

157+
// FIXME #11059 handle NaN, inf and overflow
150158
fn sum(self) -> f64 {
151-
self.iter().fold(0.0, |p,q| p + *q)
159+
let mut partials : ~[f64] = ~[];
160+
161+
for &mut x in self.iter() {
162+
let mut j = 0;
163+
// This inner loop applies `hi`/`lo` summation to each
164+
// partial so that the list of partial sums remains exact.
165+
for i in range(0, partials.len()) {
166+
let mut y = partials[i];
167+
if num::abs(x) < num::abs(y) {
168+
util::swap(&mut x, &mut y);
169+
}
170+
// Rounded `x+y` is stored in `hi` with round-off stored in
171+
// `lo`. Together `hi+lo` are exactly equal to `x+y`.
172+
let hi = x + y;
173+
let lo = y - (hi - x);
174+
if lo != 0f64 {
175+
partials[j] = lo;
176+
j += 1;
177+
}
178+
x = hi;
179+
}
180+
if j >= partials.len() {
181+
partials.push(x);
182+
} else {
183+
partials[j] = x;
184+
partials.truncate(j+1);
185+
}
186+
}
187+
partials.iter().fold(0.0, |p, q| p + *q)
152188
}
153189

154190
fn min(self) -> f64 {
@@ -955,5 +991,34 @@ mod tests {
955991
t(&Summary::new([-2.0, 0.0]), ~"-2 |[------******#******---]| 0");
956992

957993
}
994+
#[test]
995+
fn test_sum_f64s() {
996+
assert_eq!([0.5, 3.2321, 1.5678].sum(), 5.2999);
997+
}
998+
#[test]
999+
fn test_sum_f64_between_ints_that_sum_to_0() {
1000+
assert_eq!([1e30, 1.2, -1e30].sum(), 1.2);
1001+
}
1002+
}
9581003

1004+
#[cfg(test)]
1005+
mod bench {
1006+
use extra::test::BenchHarness;
1007+
use std::vec;
1008+
1009+
#[bench]
1010+
fn sum_three_items(bh: &mut BenchHarness) {
1011+
bh.iter(|| {
1012+
[1e20, 1.5, -1e20].sum();
1013+
})
1014+
}
1015+
#[bench]
1016+
fn sum_many_f64(bh: &mut BenchHarness) {
1017+
let nums = [-1e30, 1e60, 1e30, 1.0, -1e60];
1018+
let v = vec::from_fn(500, |i| nums[i%5]);
1019+
1020+
bh.iter(|| {
1021+
v.sum();
1022+
})
1023+
}
9591024
}

0 commit comments

Comments
 (0)