All posts

Programming

Giving Rust Permission to Break Floating-Point Rules

Rust 1.98.0 introduces algebraic_add, algebraic_sub, algebraic_mul, algebraic_div, and algebraic_rem for f32/f64. The important distinction is that these operations allow the compiler to reassociate/reorder floating-point calculations, potentially enabling vectorization and optimizations similar in spirit to -ffast-math. ([Rust Blog][1])

Here’s a practical example showing why this matters.

Example: summing a large array

fn normal_sum(values: &[f64]) -> f64 {
    values.iter().fold(0.0, |sum, &x| {
        sum + x
    })
}

fn algebraic_sum(values: &[f64]) -> f64 {
    values.iter().fold(0.0, |sum, &x| {
        sum.algebraic_add(x)
    })
}

fn main() {
    let values = vec![
        1.0e16,
        1.0,
        -1.0e16,
        2.0,
        3.0,
        4.0,
    ];

    let normal = normal_sum(&values);
    let algebraic = algebraic_sum(&values);

    println!("normal:    {normal}");
    println!("algebraic: {algebraic}");
}

The key difference is:

sum + x

versus:

sum.algebraic_add(x)

With ordinary floating-point addition, Rust must preserve the order of operations. For example:

(((a + b) + c) + d)

because floating-point addition is not associative.

With:

a.algebraic_add(b)
    .algebraic_add(c)
    .algebraic_add(d)

the compiler is permitted to treat the operation algebraically and potentially transform it into something equivalent to:

(a + b) + (c + d)

or another mathematically equivalent grouping. That can make parallel execution and SIMD/vectorization easier. ([Rust Blog][1])

A better example: dot product

This is where the feature becomes more interesting for performance-oriented code:

fn dot_product(a: &[f64], b: &[f64]) -> f64 {
    a.iter()
        .zip(b)
        .fold(0.0, |sum, (&x, &y)| {
            sum.algebraic_add(x.algebraic_mul(y))
        })
}

fn main() {
    let a = vec![1.0, 2.0, 3.0, 4.0];
    let b = vec![5.0, 6.0, 7.0, 8.0];

    let result = dot_product(&a, &b);

    println!("dot product = {result}");
}

Conceptually, the calculation is:

1×5 + 2×6 + 3×7 + 4×8

Normally, the compiler has to be conservative about changing the order of those floating-point operations.

Using:

x.algebraic_mul(y)

and:

sum.algebraic_add(...)

gives the optimizer permission to exploit the algebraic properties of multiplication and addition.

For a large dataset, this could potentially allow something closer to:

        ┌─ x0*y0 ─┐
        ├─ x1*y1 ─┤
        │         ├─ partial sum
        ├─ x2*y2 ─┤
        └─ x3*y3 ─┘

instead of strictly performing every operation sequentially.

All five new methods

Rust 1.98.0 provides:

let a = 10.0_f64;
let b = 3.0_f64;

let add = a.algebraic_add(b);
let sub = a.algebraic_sub(b);
let mul = a.algebraic_mul(b);
let div = a.algebraic_div(b);
let rem = a.algebraic_rem(b);

println!("add = {add}");
println!("sub = {sub}");
println!("mul = {mul}");
println!("div = {div}");
println!("rem = {rem}");

The important thing is not that these produce different mathematical results under normal circumstances. The interesting part is the optimization contract: you’re telling the compiler that you’re willing to give up strict IEEE-style operation ordering so it can optimize more aggressively. The Rust release notes explicitly describe these methods as potentially similar to -ffast-math-style optimization, while still guaranteeing that they don’t introduce undefined behavior. ([Rust Blog][1])

One important caveat: don’t use these blindly for financial, numerical-analysis, or other code where reproducible floating-point results are important. The release notes describe the operations as non-deterministic because the compiler is free to choose different valid optimizations. ([Rust Blog][1])

Rust 1.98.0 release announcement

[1]: https://blog.rust-lang.org/2026/08/20/Rust-1.98.0/ “Announcing Rust 1.98.0 Rust Blog”
Share this post
Written by Jijo Bose All posts