Rust 1.98 shipped on Thursday, August twentieth, 2026, and the headline addition is a set of algebraic floating point methods on f32 and f64. They let the compiler reorder and vectorise arithmetic using the algebraic properties of real numbers, which floating point does not actually have, in the same spirit as the fast math flag other toolchains expose. The difference is that Rust puts the choice on individual operations rather than on a whole compilation unit. The change traces back to a 2025 report that a simple dot product ran up to eight times slower than the equivalent C++ on modern x86_64. Buffered integer formatting lands alongside it.
The short answer
Rust 1.98 landed on Thursday, August twentieth, 2026. The f32 and f64 types now carry algebraic methods for addition, subtraction, multiplication, division and remainder, which let the compiler reorder and vectorise arithmetic the way a fast math flag would, but per operation rather than per compilation unit. Results become non deterministic and never unsound. Integers gain format_into with a NumBuffer for allocation free formatting, and more than twenty APIs stabilise.
Every language that takes numerics seriously eventually has this argument, and it usually ends with a compiler flag that quietly changes the meaning of every float in the program. Rust has spent years declining that bargain. In 1.98 it takes the optimisation and leaves the flag behind.
The problem was never code generation
The report that set this in motion is easy to reproduce and hard to argue with. Someone wrote a dot product in Rust, wrote the same loop in C++, and measured Rust running up to eight times slower on modern x86_64.
The generated scalar code was fine. The issue was that it stayed scalar. Floating point addition is not associative, so when you write a sum as a + b + c + d, the language is obliged to evaluate it as ((a + b) + c) + d. A serial dependency chain like that cannot be split across vector lanes, so the loop never vectorises. The C++ build, typically compiled with permissive floating point settings, was free to regroup and did.
That is not a Rust code generation weakness. It is Rust honouring the semantics it promised, in a place where most of its competition quietly does not.
What the new methods change
The f32 and f64 types now expose algebraic variants of the five arithmetic operations. Write the same sum as a chain of algebraic_add calls and the compiler may regroup it, perhaps into (a + b) + (c + d), so the two partial sums evaluate at the same time. Once the dependency chain is broken, broader loop vectorisation usually follows on its own.
The release notes are careful about what they are and are not promising. The exact set of optimisations is not specified, only that it may resemble what a fast math flag does elsewhere. So you are asking for a class of transformation, not signing a contract about which ones the backend picks today.
Two properties matter more than the speed:
- The methods are non deterministic. The same inputs may produce different results even within a single run of the same program, because the compiler is free to choose differently in different inlined contexts.
- They never cause undefined behaviour. Unsafe code must not rely on any property of the return value for soundness, but nothing here can corrupt memory.
That second point is what makes this a reasonable thing to ship in a language whose selling point is that you can trust the guarantees. The failure mode is a number you did not expect, not a heap you cannot trust.
Why the call site is the right place for this
The usual way to get these gains is a compiler flag, and the usual cost is that the flag applies to everything it can see. Turn it on to speed up one hot accumulator and you have also loosened the guarantees under a currency calculation three modules away, a comparison in a test helper, and any dependency compiled in the same unit.
Putting the decision on the method call inverts that. You rewrite the loop you profiled, and the rest of the program keeps ordinary IEEE 754 semantics. Reviewers see the change in the diff rather than in a build script. Someone reading the code in two years can tell which arithmetic was deliberately loosened.
The practical consequence is that this is now a targeted tool, not a global switch. Profile first, find the reduction that will not vectorise, rewrite that loop, measure again. If the numerics in that loop are a sum of similar magnitude terms, the reordering error is usually negligible. If it is a subtraction of nearly equal quantities, or an accumulation across values that span many orders of magnitude, leave it alone.
Buffered integer formatting
The other performance change in 1.98 is smaller and will show up in more codebases. Primitive integer types gain a format_into method that accepts a NumBuffer<Self>.
The point is avoiding the dynamic dispatch that the standard formatting machinery goes through. Writing an integer into a caller supplied buffer skips that layer entirely, and the release notes put the result at the level of specialised crates like itoa. Anyone who has pulled in a dependency purely to serialise integers quickly in a hot logging or encoding path can now drop it.
The rest of the release
More than twenty APIs stabilise. The ones likely to appear in ordinary code are the range and substring helpers, substr_range, subslice_range and strip_circumfix, along with UTF-16 conversions through from_utf16le and from_utf16be and their lossy variants, mutable access methods on atomic types, and radix parsing for NonZero integers.
Documentation also picked up a stable guarantee about moving out of a dropped Box wrapped in ManuallyDrop, which formalises a fix that shipped back in 1.96.0. Nothing changes at runtime, but the behaviour you were probably already relying on is now something you are allowed to rely on.
What to do with this release
Upgrade normally, since nothing here is a breaking change. Then, if you have numerical code, go and look at the reductions rather than the individual operations. The gains from the algebraic methods concentrate in loops that accumulate, because those are the ones where the serial dependency was blocking vectorisation in the first place.
We would also write down, next to any algebraic call you add, why the reordering is acceptable for that particular calculation. Not for the compiler, which does not care, but for the person who later finds a test that passes on one machine and fails on another and needs to know within thirty seconds whether this loop is the reason.
Sources and further reading
- Announcing Rust 1.98.0, The Rust Blog, August 20, 2026
- Rust 1.98 Adds Algebraic Floating-Point Methods Akin To ffast-math, Phoronix, August 20, 2026
Frequently asked questions
What exactly are the algebraic floating point methods?
They are methods on the f32 and f64 primitive types covering addition, subtraction, multiplication, division and remainder, named algebraic_add and so on. Calling them tells the compiler it may optimise using the algebraic properties of real numbers, even though those properties do not hold for floating point values. The canonical example is associativity: a + b + c + d must be evaluated left to right as written, but a chain of algebraic_add calls can be regrouped as (a + b) + (c + d) so the partial sums compute in parallel. Broader loop vectorisation often follows.
Is this the same as compiling with fast math?
It is the same family of optimisation with a much narrower blast radius. A fast math flag applies to an entire compilation unit, so every floating point operation in scope loses its guarantees whether you thought about it or not. Rust puts the decision at the call site, which means you opt a specific accumulator loop into reordering and leave the rest of your numerics alone. The exact set of optimisations is deliberately unspecified, so you get the class of transformation rather than a contract about which ones run.
Can these methods introduce undefined behaviour?
No. The documentation is explicit that these methods are non deterministic, because the compiler is free to choose different optimisations, but that they never cause undefined behaviour. It also warns that the same inputs may produce different results even within a single program run, and that unsafe code must not rely on any property of the return value for soundness. So the risk is numerical, not memory safety: results can shift, and anything downstream that treats a float comparison as a correctness invariant needs looking at.
Where did the 8x figure come from?
From a 2025 issue reporting that simple dot products in Rust ran up to eight times slower than C++ on modern x86_64 processors. The cause was not code generation quality in general, it was that the compiler could not legally reorder the floating point accumulation, so the loop never vectorised while the C++ build, usually compiled with permissive floating point settings, did. The algebraic methods exist to give Rust an in language way to grant that permission.
What else is in Rust 1.98?
Buffered integer formatting arrives: primitive integer types gain a format_into method taking a NumBuffer, which avoids the dynamic dispatch of the usual formatting machinery and reaches the performance of a dedicated crate such as itoa. Documentation now gives a stable guarantee about moving out of a dropped Box wrapped in ManuallyDrop, formalising a fix from 1.96.0. More than twenty APIs stabilise, including substr_range, subslice_range, strip_circumfix, the from_utf16le and from_utf16be conversions with lossy variants, mutable access methods on atomics, and radix parsing for NonZero integers.