August 20, 2026·5 min read·engineeringrustnumericsperformance

Rust 1.98 makes algebraic floating-point optimization explicit

Rust 1.98 adds algebraic floating-point methods and caller-owned integer formatting buffers, while documenting an earlier ManuallyDrop fix.

Adding floating-point numbers in a different order can change the answer. Rust 1.98 gives programmers an explicit way to permit optimizations that use the algebraic properties of real numbers despite that limitation. The August 20 announcement introduces methods such as algebraic_add on f32 and f64. Ordinary arithmetic keeps its existing meaning.

This is a useful design choice for a systems language. A program can identify the operations where it accepts a different numerical contract. The compiler receives permission at those operations, and a reader can see that permission in source. There is still a substantial numerical decision to make, but the API gives that decision a concrete location.

Rust's semantics are a large part of its appeal to me. Go remains my usual first choice for many general-purpose tools, while Rust is increasingly persuasive where control over representation and execution matters. Explicitly naming a relaxed arithmetic operation fits that preference. It makes the cost in numerical guarantees part of the program a reviewer reads.

Reassociation changes the dependency chain

The release announcement illustrates a sum parsed as ((a + b) + c) + d. Each addition depends on the preceding result. With algebraic addition, the compiler may choose a grouping such as (a + b) + (c + d), allowing independent partial sums. The same permission can enable broader loop vectorization.

The new spelling is available directly on a floating-point value, as the 1.98 API documentation records:

fn algebraic_sum(a: f64, b: f64, c: f64, d: f64) -> f64 {
    a.algebraic_add(b).algebraic_add(c).algebraic_add(d)
}

This example permits an optimization. It does not force one particular evaluation tree. The announcement leaves the exact set of transformations unspecified and describes the operations as nondeterministic. Their results must consequently be assessed under the relaxed contract, rather than under an assumption that the source spelling fixes every intermediate rounding step.

That distinction matters for interpreting a performance improvement. A different execution strategy may be possible because the program accepts different answers. Comparing the new method with ordinary addition is therefore a comparison of both implementation opportunities and numerical requirements. An application that needs a fixed evaluation order has a different requirement from one that accepts reassociation.

Numerical correctness remains an application question

The release states that algebraic methods never cause undefined behavior. That guarantee is important, but it does not say every result is suitable for every algorithm. A computation can remain within the language's rules while violating an application's required tolerance or reproducibility policy.

For example, an API that exposes a numerical result needs to decide whether callers can compare that result across builds. If its implementation switches to algebraic operations, that expectation becomes part of the compatibility discussion. The change may be acceptable, but the API owner needs to state the permitted variation. The method name communicates a local choice, while the surrounding API communicates its consequences to users.

The same applies to intermediate decisions inside an algorithm. A small difference in a sum can affect a later comparison. Whether that changes an acceptable approximation or an unacceptable control decision depends on the algorithm. The compiler cannot infer the application's intended error budget merely because a faster evaluation is available.

My preference is to keep this permission near the numerical code that justifies it. The new API supports that style without requiring the reader to discover a global build option before understanding why arithmetic may be reassociated. The release also includes algebraic subtraction, multiplication, division and remainder, so the explicit choice extends beyond sums.

Integer formatting gains a different form of explicit control

The release's integer formatting API makes storage ownership visible. Primitive integers now provide format_into, taking a mutable reference to NumBuffer<Self> and returning a string slice borrowed from that buffer. The versioned NumBuffer documentation says its internal capacity depends on the maximum number of digits of the associated integer type.

The i32 method documentation specifies signed decimal formatting. A complete use of the interface is small:

use core::fmt::NumBuffer;

fn main() {
    let mut buffer = NumBuffer::new();
    let value = -1972_i32;
    let text = value.format_into(&mut buffer);
    assert_eq!(text, "-1972");
}

The returned text belongs to the buffer's borrow. That relationship explains how the caller can supply storage without the method returning an owned string. It also makes the storage lifetime part of ordinary Rust borrowing, which is easier to reason about than an undocumented scratch buffer hidden inside a formatting helper.

The announcement says this route avoids much of the dynamic dispatch associated with buffered write! formatting. That is a specific reason it can help a narrow conversion path. It does not make the method a replacement for general formatting requirements. Its useful scope is a decimal integer representation in caller-provided storage.

The opaque buffer also keeps capacity calculation out of the calling code. A caller chooses the integer type and receives storage sized for that type's representation. That removes a separate local calculation about the longest possible number, including the sign for a signed integer. It is a small API, but its type relationship captures information that a manually sized byte array would leave to the programmer.

The ManuallyDrop guarantee documents an earlier fix

Rust 1.98 also updates the documented contract for ManuallyDrop. Before Rust 1.96, manually dropping a contained Box and subsequently moving its ManuallyDrop wrapper could be undefined behavior. The compiler fix landed in 1.96. The 1.98 change adds the stable documentation guarantee that this move remains permitted.

The 1.98 ManuallyDrop documentation preserves the distinction between moving the wrapper and using its destroyed contents. Its unsafe drop method still requires callers to prevent invalid later use and repeated destruction. Permission to move the wrapper does not recreate the value that was dropped.

That documentation change matters to authors of unsafe abstractions because a stable guarantee can support a design beyond the behavior of one compiler build. The version history matters equally. A library supporting older compilers cannot treat the 1.98 announcement as evidence that the fix first appeared there, or that compilers before 1.96 have the same behavior. The release adds new performance APIs while making this earlier semantic correction an explicit part of the documented contract.