为何使用f64::mul_add会降低精度?如何有效使用该方法?
f64::mul_add Hurt Your Dot Product Precision (and How to Fix It) Great question—this is a common gotcha with fused multiply-add operations, especially when applied to sums of products like dot products. Let's break down what's happening and how to use mul_add effectively.
The Root Cause: Nested mul_add Breaks Compiler Optimizations
First, let's recap: f64::mul_add computes self * a + b with one rounding step instead of two (the separate multiply-then-add does one round after multiplication, another after addition). This is perfect for operations that fit the exact a*b + c pattern, but your dot product is a sum of four products—not a chain of multiply-adds.
Your nested mul_add implementation:
self.x.mul_add(v.x, self.y.mul_add(v.y, self.z.mul_add(v.z, self.w * v.w)))
forces rounding to f64 precision at every step:
- First,
z * v.z + w * v.wis rounded tof64 - Then,
y * v.y + [that result]is rounded again tof64 - Finally,
x * v.x + [that result]is rounded a third time tof64
In contrast, your original dot product code:
self.x * v.x + self.y * v.y + self.z * v.z + self.w * v.w
gets optimized by rustc (especially in --release mode) to use extended precision for intermediate sums (like 80-bit x87 floats on x86 systems). This means the cumulative sum isn't rounded to f64 until the very end, reducing the total number of rounding errors. By using nested mul_add, you're overriding this optimization and introducing errors earlier—leading to the larger discrepancy in your test.
How to Use mul_add Effectively for Linear Algebra
mul_add is still a powerful tool for your library—you just need to apply it to the right operations.
1. Use mul_add for Single Linear Operations
For operations that naturally fit the a*b + c mold (like calculating a single component of an affine transform), mul_add works perfectly. For example:
// Calculating one component of a vector transformed by an affine matrix let transformed_x = mat.m00.mul_add(vec.x, mat.m01.mul_add(vec.y, mat.m02.mul_add(vec.z, mat.m03)));
Here, each mul_add is computing a term that directly contributes to the final sum, and the nesting makes sense because each step is a true multiply-add operation.
2. Combine mul_add with High-Precision Summation for Dot Products
If you want to keep using mul_add for dot products while minimizing error, pair it with a Kahan summation algorithm (a method to reduce cumulative rounding error in sums). Here's how that would look for your dot method:
impl AffineVector { pub fn dot(self, v: AffineVector) -> f64 { let mut sum = 0.0; let mut correction = 0.0; // Process each (x*v.x) term with mul_add and Kahan correction let term = self.x.mul_add(v.x, 0.0); let y = term - correction; let new_sum = sum + y; correction = (new_sum - sum) - y; sum = new_sum; // Repeat for y, z, w components let term = self.y.mul_add(v.y, 0.0); let y = term - correction; let new_sum = sum + y; correction = (new_sum - sum) - y; sum = new_sum; let term = self.z.mul_add(v.z, 0.0); let y = term - correction; let new_sum = sum + y; correction = (new_sum - sum) - y; sum = new_sum; let term = self.w.mul_add(v.w, 0.0); let y = term - correction; let new_sum = sum + y; correction = (new_sum - sum) - y; sum = new_sum; sum } }
This way, you get the single-rounding benefit of mul_add for each product, plus the error correction from Kahan summation to keep the sum accurate.
3. Ditch Exact Equality Checks for Floats
Your test failure is also due to using assert_eq! on floating-point values—this is almost never a good idea, since tiny rounding errors are unavoidable. Instead, use approximate equality with a small epsilon:
// Replace your final assert_eq! with this assert!((returned.x - 1.0).abs() < 1e-15); assert!((returned.y - 0.0).abs() < 1e-15); assert!((returned.z - 0.0).abs() < 1e-15);
For cleaner code, you could also use a crate like approx which provides helper macros for floating-point comparisons.
4. Trust Compiler Optimizations for Simple Sums
For straightforward dot products like your original implementation, enabling optimizations (cargo build --release) will often give you better precision than manual mul_add nesting. The compiler will automatically use extended precision for intermediate sums where available, reducing rounding error without any extra code from you.
Wrap-Up
mul_addis designed for singlea*b + coperations, not nested sums of products.- Nested
mul_addforces early rounding, overriding compiler optimizations that use extended precision for intermediates. - For dot products, pair
mul_addwith Kahan summation, or let the compiler optimize the original sum. - Always use approximate equality checks for floating-point tests—exact equality is fragile.
内容的提问来源于stack exchange,提问作者Kelson Ball

