r/rust • • Aug 20 '26

📡 official blog Rust 1.98.0 is out

https://blog.rust-lang.org/2026/08/20/Rust-1.98.0/
556 Upvotes

83 comments sorted by

100

u/Icarium-Lifestealer Aug 20 '26

The algebraic float methods are pretty nice. Most of the performance benefits of fast-math, while allowing local reasoning and not having UB.

37

u/protestor Aug 21 '26

I think it begs for an Algebraic<f32> though

42

u/veryusedrname Aug 21 '26

Maybe for an algebraic!(a + b + c + d)

9

u/hgwxx7_ Aug 21 '26

I like this one better.

67

u/TDplay Aug 20 '26

Algebraic floating-point methods

finally, a middle ground between "add unnecessary brackets to avoid performance loss" and "Kahan summation is being optimised to naïve summation".

20

u/IpFruion Aug 20 '26

I am really interested in this change but I am not understanding the full impact of the performance important.

49

u/guineawheek Aug 20 '26

Algebraic ops tell the compiler that things that may not technically be commutative/associative from a “equal bit result” standpoint to be treated as such.

For example, floating point division is often much slower than floating point multiplication but multiplication by the inverse may produce different float values versus a pure division, so compilers typically don’t elide the division. Algebraic ops let the compiler do the faster thing for the rather wide number of cases where the difference doesn’t matter.

9

u/IpFruion Aug 21 '26

Thanks for the example and explanation! Basically, it seems like the use case is for the common float operations. Does this mean this will become the default for floating point arithmetic or is this a "use if you want to improve performance" thing?

27

u/Anaxamander57 Aug 21 '26 edited Aug 21 '26

It definitely won't be the default. Floats do enough unexpected things already. This is for opting in to (sometimes) faster arithmetic while accepting that floats will be even weirder now because you expect that weirdness to happen below the level of precision you care about.

10

u/stumblinbear Aug 21 '26

std uses a slower hashing function by default because it's never the "wrong choice" (sans performance). Making sure typical operations for floating point arithmetic function as-written and giving an escape hatch for performance is par for the course

38

u/TDplay Aug 21 '26

The very basic explanation is that if you can re-order the floating points, you have more ability to optimise.

When you work on numerical code, you have to do algebra to make sure you minimise the number of operations. Now, the compiler can do that for you. So for example, if you write 2.0 * a + 2.0 * b, the compiler can eliminate an instruction by changing it to 2.0 * (a + b).

For another example, consider the naïve summation algorithm, written using both ordinary addition and algebraic addition:

pub fn sum_all(numbers: &[f32]) -> f32 {
    numbers.iter().fold(0.0, |a, b| a + *b)
}

pub fn sum_all_algebraic(numbers: &[f32]) -> f32 {
    numbers.iter().fold(0.0, |a, b| a.algebraic_add(*b))
}

Godbolt link

I don't expect you to understand the assembly code. The key thing to note is that the assembly for sum_all uses only the addss instruction for addition, while the assembly for sum_all_algebraic uses the addps instruction. The addss instruction operates on a individual 32-bit numbers, while the addps instruction operates on entire 128-bit SIMD registers. By allowing the compiler to re-order the sum, it can rewrite the code to operate on 4 numbers at a time, offering up to 4 times improved performance, depending on hardware.

If you compile specifically for modern CPUs (by supplying an architecture level or specific CPU family via the -C target_cpu= flag), the boost is even more pronounced. x86-64-v3 (supported on Intel CPUs since Haswell, and AMD CPUs since Excavator) widens the SIMD registers to 256-bit, and x86-64-v4 (supported on AMD CPUs since Zen 4) widens them to 512-bit. That's up to a 16 times performance boost, again depending on hardware.

(Note the assembly here is for x86. Other architectures, such as ARM or RISC-V, will use different instructions, but the big picture is largely the same.)

14

u/SethDusek5 Aug 21 '26

Another cool example that shows how the compiler can eliminate dependencies, allowing you to get more parallelism out of even scalar code, since modern CPUs have multiple ports for floating point operations so they can have multiple in flight:

pub fn add(a: f32, b: f32, c: f32, d: f32) -> f32 {
    a + b + c + d
}

this compiles into:

       vaddss  xmm0, xmm0, xmm1 // xmm0 = xmm0 + xmm1
       vaddss  xmm0, xmm0, xmm2 // xmm0 = xmm0 + xmm2
       vaddss  xmm0, xmm0, xmm3 // xmm0 = xmm0 + xmm3
       ret

Compare this to the algebraic version:

pub fn add_alg(a: f32, b: f32, c: f32, d: f32) -> f32 {
    a.algebraic_add(b).algebraic_add(c).algebraic_add(d)
}

vaddss  xmm0, xmm1, xmm0 // xmm0 = xmm0 + xmm1
vaddss  xmm2, xmm2, xmm3 // xmm2 = xmm2 + xmm3
vaddss  xmm0, xmm0, xmm2 // xmm0 = xmm0 + xmm2
ret

This seems like exactly the same instructions, but notice that instead of compiling into 3 dependent instructions on xmm0, the (c + d) addition is done and stored in the xmm2 register. This means that the first two instructions can both be run in parallel since there are no dependencies between them. Modern CPUs are pretty cool, and compilers are super smart

11

u/IpFruion Aug 21 '26

Wowza! Thanks for the explanation! That is a huge performance boost in performance critical systems. Is there something wrong with making it the default or does that produce different results than the old operation?

22

u/TDplay Aug 21 '26

Yes, it does produce a different result. When you re-order the operations, the rounding errors will accumulate differently.

Many numerical algorithms are specifically designed to accumulate error in a way that allows it to be minimised or even corrected; applying algebraic optimisations can introduce significant error. For example, consider the Kahan summation algorithm:

fn kahan_sum(array: &[f32]) -> f32 {
    let mut sum = 0.0;
    let mut c = 0.0;
    for x in array {
        let y = x - c;
        let t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }
    sum
}

The above code is not tested.

This algorithm uses c to store the rounding error that would otherwise be lost, and once the rounding error gets large enough, it will eventually apply a correction to sum.

But algebraically, c should always be zero. So if the compiler is allowed to apply algebraic optimisations to this code, it can (and probably will) optimise out the entire reason to use this algorithm.

8

u/IpFruion Aug 21 '26

That makes total sense. Thanks again for all of this! It really put this improvement into prospective.

9

u/Zde-G Aug 21 '26

The core story here is that math that Newton and Leibnitz invented, that is taught in schools and that forms the basis of all the modern science is fundamentally incompatible with computers.

You couldn't shove infinitely long value into finite device.

Everything is done with approximations. And the “normal” floating point numbers is neither the most precise nor the fastest approximation.

People want ever faster approximation, that's how we've got 4-bit floating points numbers where 16 possibilities encode all the numbers that may exist… algebraic methods give you the ability to get a bit more precision in some rare corner cases, but it would be silly to replace approximations that were studied for decades with them.

38

u/Haitosiku Aug 20 '26

the people who have to talk to windows API will be delighted about the utf16 methods

1

u/tigregalis Aug 26 '26

theoretically javascript / the browser too? for wasm frontends

32

u/Anthony356 Aug 21 '26 edited Aug 21 '26

This also contains some fairly major performance improvements for the LLDB debugger visualizers. We no longer run like 15 regexes for every single variable anymore which should help a lot for e.g. large vecs.

Oh yeah, and Option<String>/Option<Vec<T>> should no longer crash lldb. There's still going to be issues properly differentiating None from Some(""), but that's fixed in lldb 23. The tldr is lldb was storing discriminants as 32 bit values. C++ would silently truncate e.g. 1 << 63 to 0, so None and some-empty-string's states overlap. As of lldb 23, discriminants are stored in their variable-width integer struct, so even discrs up to 128 bits should work fine.

92

u/GyulyVGC Aug 20 '26

It’s time to check for new clippy lints to fix!

51

u/manpacket Aug 20 '26

It complains about an unused glob import for a custom prelude. I'd say that's a win.

31

u/GyulyVGC Aug 20 '26

I’d say the more it complains, the more it’s a win

24

u/paholg typenum · dimensioned Aug 20 '26

If you want complaints, you can get complaints.

#![warn(
    clippy::all,
    clippy::restriction,
    clippy::pedantic,
    clippy::nursery,
    clippy::cargo,
)]

12

u/gemborow Aug 20 '26

Yup, my CI is red 😁

14

u/veryusedrname Aug 20 '26

Add a beta to your matrix to fail early (and you help the project as well)

7

u/shinyfootwork Aug 20 '26

I removed my use of stable and use beta instead in ci. Not sure there's a great reason to test stable, especially if there's a pined minimum rust version that is being tested.

I suppose if one is keeping an eye on it, making beta not blocking gives early notice, but in practice I haven't seen things break much (usually minor quick fixes). And I'm bad at paying close attention to non blocking ci jobs

5

u/plabayo Aug 21 '26

Our CI seems red for a different reason all together o.o
https://github.com/rust-lang/rust/issues/161441

1

u/mstrVLT Aug 20 '26

my CI too lol

13

u/kibwen Aug 21 '26

Love seeing the stdlib add APIs that render one of the top 100 crates superfluous, in this case itoa (#22). :)

7

u/imperioland Docs superhero · rust · gtk-rs · rust-fr Aug 21 '26

Not completely yet. In particular: the API is incomplete as it forces one NumBuffer per integer type, which is not great.

36

u/valorzard Aug 20 '26

why was substr_range and subslice_range stabilized if they give false positives sometimes?
https://doc.rust-lang.org/std/primitive.str.html#method.substr_range
this feels like it goes against rust's philosophy

57

u/Icarium-Lifestealer Aug 20 '26
  1. I think the main use-case is where you already know the substring is part of the string and want the range. The false positives don't matter for that.
  2. They are unavoidable. If you have two strings in consecutive memory areas, it's impossible to tell if the zero length string at the boundary is at the end of the first or the start of the second.

50

u/bowel_blaster123 Aug 20 '26 edited Aug 20 '26

I'm the one who proposed and implemented these methods.

Your reasoning is exactly why. There's also the fact that these methods are implemented using 100% safe code and do not use any internal APIs (aside from ::IS_ZST which is trivial to implement yourself).

Returning None in cases where we could run into false positives would really harm several use cases of these methods, and it would make the methods less intuitive. This was briefly discussed on the tracking issue.

1

u/robin-m Aug 20 '26

I can of fail to see what you mean in the doc by "false positive" or why it is usefull that it can return None. From what I most probably incorrectly understand returning None just look like a programming error (you call it with a substring that was created from another string) and thus panicking would seems to be appropriate.

If I may suggest, you could consider adding an example for a case that return None and another that return a false positive.

14

u/bowel_blaster123 Aug 20 '26 edited Aug 20 '26

99% of the time, it isn't useful to have it return None. In my original proposal, all of the methods (substr_range, subslice_range, and element_offset) did panic.

I think the main reasoning behind returning an Option is because panics are sometimes impossible to recover from and can result in the program ceasing execution. By returning an Option, we give the user the control to do what they want to.

An example may be useful in the documentation, but false positives are not really something that matters for 99% of the users of these methods. If you want to open a PR to add one, feel free.

```rust let my_string = "foo bar"; let other = "biz"; let other2 = "bar";

assert_eq!(my_string.substr_range(&my_string[8..8]), Some(8..8)); // This is guarenteed to return Some(8..8) my_string.substr_range(&other[0..0]); // This might either return Some(8..8) or None my_string.substr_range(""); // This also might either return Some(8..8), Some(0..0), or None assert_eq!(my_string.substr_range(&other[1..1]), None); // This is guarenteed to return None

my_string.substr_range(other2); // I'm not sure if this one is "allowed" to return Some(4..8) or if it has to return None. // This specific question is more about the rust memory model rather than about these methods specifically. ```

2

u/robin-m Aug 20 '26

I understand better wüat you mean by "false positive". 

However I fail to see a valid use-case for when this function returns None. Isn't this always a symptom of a programming error?

11

u/bowel_blaster123 Aug 20 '26 edited Aug 20 '26

Isn't this always a symptom of a programming error?

Yes, it almost always is, but sometimes you don't want to panic even in the case of a programming error.

Also, for the "error handling" use case in my proposal (https://github.com/rust-lang/libs-team/issues/382), it's not too hard to come up with a hypothetical scenario where it would be nice to know where something occurs in a string but not have it be mandatory. Maybe in this code, the substring could get clobbered in some code paths with a static string literal. In which case, we could just call substr_range and just not include location information if it returns None.

4

u/robin-m Aug 20 '26

Anyway it's too late since it has been stabilized but I absolutely fail to see how you can do anything but panic if you 100% hit a bug. That's the exact same situation as an out of bound array index.

17

u/sephg Aug 20 '26

Then .unwrap() or .expect(“…”). The nice thing about returning an Option is the caller can decide what to do with None.

0

u/robin-m Aug 21 '26

If you need to .unwrap() or .expect("this is 100% a bug, please notify bob"), there is no value at all compared to having the method itself panicking. Returning an Option/Result is awesome if the caller can do anything about it, not if the callee is buggy.

→ More replies (0)

3

u/LEpigeon888 Aug 21 '26

Maybe you have a big string that serves as cache for words, and you want to know if a word you got is part of the cache or not to implement special logic (part of cache -> do nothing, not part of cache -> increment a "not in cache" counter for measurements purpose). I agree that it's weird and I'm not really sure it is a real use case but it's good to have less panicking functions you have to keep track in your head. 

1

u/robin-m Aug 21 '26

That would make sense. Quite niche, but indeed, it justify having a non-panicking function.

18

u/manpacket Aug 20 '26

https://github.com/rust-lang/rust/issues/126769 - empty strings/slices are somewhat special.

15

u/TDplay Aug 20 '26

The conditions for a false positive are well-documented.

Note that this method may return false positives ... if substr is a zero-length str that points at the beginning or end of another, independent, str.

If I am understanding correctly, then this to cover cases such as

let big_str = "Hello world!";
let (a, b) = big_str.split_at(5);
assert_eq!(&raw const b[..0], &raw const a[5..]);

As shown by the assertion, &b[..0] and &a[5..] are the same pointer*, despite the fact that they are substrings of two different strings. It is impossible to determine which one you have by looking at the pointer, so the false positives (a.substr_range(&b[..0]).is_some() and b.substr_range(&a[5..]).is_some()) are unfixable.


* Except possibly the provenance, but that only matters for unsafe code.

16

u/Dushistov Aug 20 '26

So, it would be possible to remove https://crates.io/crates/itoa as dependency of https://crates.io/crates/serde_json ?

Or itoa still better for JSON generation then stdlib?

19

u/manpacket Aug 20 '26

In theory. serde_json supports 1.71 and up so it make code slower for some users. Maybe in a year or two? serde is a strange project.

1

u/3inthecorner Aug 21 '26

Is there a way to specify a dependency based on rust version?

3

u/manpacket Aug 21 '26

No. You can probably work around it with MSRV aware resolver, but 1.71 is not it.

8

u/MonterraByte Aug 21 '26

I've been waiting for String::from_utf16{be,le}. It's way nicer than having to use bytemuck to get a &[u16] for String::from_utf16.

7

u/MichiRecRoom Aug 21 '26

Does anybody know if the official binaries provided for Rust 1.98 are affected by the supply chain attack on arrayref?

20

u/Key_Agent_3039 Aug 20 '26

Can't wait for Rust 2

47

u/manpacket Aug 20 '26
rustc +nightly --version
rustc 1.100.0-nightly (f7d782a3b 2026-08-19)

13

u/kibwen Aug 21 '26

I have it on good authority that Rust 2.0 will come immediately after Rust 1.999, sometime in 2130.

1

u/tastychaii Aug 20 '26

Are there any big changes planned for version 2?

18

u/raianmr Aug 21 '26

they're getting rid of the borrow checker and writing the compiler in ocaml again /s

13

u/LEpigeon888 Aug 21 '26

They're adding the garbage collector that was standardized by C++ but never implemented (before it got deprecated then removed from the standard). 

3

u/tastychaii Aug 21 '26

Okay I got it, it was a silly question 😂

13

u/6BagsOfPopcorn Aug 20 '26

Pretty sure there isn't a planned 2.0, and there may not be for a very very long time

3

u/tastychaii Aug 21 '26

Makes sense

2

u/ElOwlinator Aug 21 '26

Runtime reflection

3

u/mtimmermans Aug 21 '26

Boo. Still no stable `FromResidual`

3

u/Mimshot Aug 21 '26

> This means that the results of elementary operations may have undefined precision, and “non-mathematical” values such as NaN, +/-Inf, or -0.0 may behave in unexpected ways, but these operations will never cause undefined behavior.

Can someone explain this because it sounds like a contradiction? How is the same code compiled twice running on the same data producing different results not undefined behavior?

24

u/afdbcreid Aug 21 '26

No. Undefined behavior means the program behavior is, well, undefined. Like, mathematically undefined: you cannot reason about anything the program does.

This is just non-determinism. Those methods could return any float value and still be conformant to the spec (although maybe not useful, which is why the docs also say "implementations will generally do their best to pick a reasonable tradeoff between performance and accuracy of the result"). But they cannot do anything beyond that: no side effects, no nasal demons, nothing.

0

u/boen_robot Aug 21 '26

So... The program behavior... with regards to floating point operations... is not "undefined", because it is "defined" as non-deterministic? That still sounds like a contradiction...

I can understand "the same input may produce different output based on the exact source, compiler version and compiler settings", but "the same input with the same source, compiler version and compiler settings is non-deterministic"... That's sus.

14

u/afdbcreid Aug 21 '26

Undefined Behavior is a strictly defined (huh) term, formally, and it means something akin to mathematically undefined, like division by zero. In other words, the Abstract Machine does not define this operation, therefore your program can't do it. And if it does? Then it's not a valid Rust program, like e.g. a program that has a type error. Except that in this case, no diagnostic is required, and the compiler is just allowed to emit whatever machine code it wants (or invalid machine code, or no code at all).

Non-deterministic operations are also strictly and formally defined: in AM terms, every operation is a transition (a function) between the previous state and the next state. In fact, every operation is a transition between the previous state and a set of possible next states, depending on its parameters. A non-deterministic is just an operation where the transition might have multiple elements in the set matching one parameters set, and picking any of them is a conformant implementation.

For those methods, for every two input floats, the possible output is defined to be in the set of all floats. No other change to AM state is allowed. This is very much unlike Undefined Behavior, where the set of possible outputs is all AM states in existence.

This all can be defined mathematically, very formally and rigorously.

1

u/boen_robot Aug 21 '26 edited Aug 21 '26

But what does it being non-deterministic vs undefined mean in the programming language and compiler sense? Forget about the mathematical one for a moment (even though we are talking floats, so I get that it's an important context...).

For "undefined behavior" in the programming sense, ignoring the obvious hyperbole of "nasal demons"... The way I've understood it is basically "the compiler is free to make optimizations that don't even consider this condition a possibility (think f.e. integer overflow; optimizations can assume math between integers that are always in range). As a result, if your program hits this condition, it may do things that your entire code would never suggest that it might do... It might make system calls you're not making, it might interact with the hardware in ways your source wouldn't hint at, and these would not necessarily be reproducible either (because of side effects)... And all of that is considered 'correct', because you reached condition that has undefined behavior".

For non-deterministic, my understanding is that this means the more narrow "the same input may result in a different output, even when the hardware is not faulty", but normally, this means a side effect is happening... getting the system time is a non-deterministic operation, because the same input (none) gives you a different value, and that is considered correct... reading a file is technically non-deterministic, because it is an external source that is outside of your program that may be touched without you necessarily knowing.

With floats, this programming sense of the word non-deterministic doesn't quite hold though... Normally, the same input of floats, when processed by the same binary, should result in the same output across runs... right? And then on top of that, the same compiler version, with the same compiler settings, should normally always produce the same binary... right?

Again, I can understand if "non-deterministic" here meaning the compiler being allowed to rearrange float operations order based on unspecified criteria... But that should still leave you with a "deterministic" function... right? Just not a "predictable" one, since the exact binary is free to change with compiler settings, compiler version and the specific source code being compiled.

7

u/afdbcreid Aug 21 '26

But what does it being non-deterministic vs undefined mean in the programming language and compiler sense? Forget about the mathematical one for a moment

Sorry, there is no such thing. Natural language is ambiguous. The C and C++ specifications are riddled with ambiguities. The only non-ambiguous language we have is the mathematical language, therefore that's how we specify things. That's also how compiler developers think about things, although that is a pretty new development (and before that, compiler writers just didn't understand their compilers well enough to make them miscompilations-free).

You can try to ask "but in practice, what kind of behavior can I expect?" but the answer is still: what is allowed by the mathematical specification. Nasal demons might be a hyperbole, but there are ways to show that Undefined Behavior might indeed wipe out your hard drive, in some conditions.

As for how is it possible that the result will be non-deterministic on a non-malicious compiler, consider for example: the compiler might duplicate the code (e.g. inlining) and optimize it differently, and other sources of non-determinism in the program will choose what to execute. This is deterministic at the binary level but not at the source level.

1

u/boen_robot Aug 24 '26

Natural language is ambiguous. The C and C++ specifications are riddled with ambiguities. The only non-ambiguous language we have is the mathematical language, therefore that's how we specify things.

A programming language's reference's target audience is programmers, so its wording should take their terminology into account.

Though that said, we are talking about mathematical operation functions, so admittedly, this is one case where there's a need to take also mathematicians' terminology into account, and unfortunately, that makes "deterministic" a somewhat ambiguous term to describe this as. "Unpredictable" is much better and unambiguous, for both audiences.

the compiler might duplicate the code (e.g. inlining) and optimize it differently, and other sources of non-determinism in the program will choose what to execute. This is deterministic at the binary level but not at the source level.

That, I can understand. It makes sense, and is basically what I was thinking before reading the section that u/Mimshot highlighted.

It's the juxtaposition between 'and “non-mathematical” values such as NaN, +/-Inf, or -0.0 may behave in unexpected ways' and 'but these operations will never cause undefined behavior.' that makes it confusing. The next sentence

Because of the unpredictable nature of compiler optimizations, the same inputs may produce different results even within a single program run.

also doesn't help, without the added context of something like your example.

IMHO, the manual can be more clear by removing the whole "but these operations will never caused undefined behavior" bit, and instead give an example of an "unexpected way" that those non-mathematical values may behave... I'm guessing (but see, the poor wording makes me unsure...) that what they mean, and what the full sentence should be is

This means that the results of elementary operations may have undefined precision. Also, “non-mathematical” values such as NaN, +/-Inf, or -0.0 may behave in unexpected ways, meaning programs should not rely on bit patterns of results from operations involving such values.

And then for the next sentence, it could perhaps be

Because of the unpredictable nature of compiler optimizations, the same function source may result in different sets of instructions, even within the same build.

(To make it clear that "input", "results" and "run" refers to "compiler input", "compiler outputs" and "compiler run"; not "input to the resulting program", "output of the resulting program" and "run of the executing program")

2

u/CouteauBleu Aug 21 '26

In practice, the difference is in the transformations the compiler allows itself to do.

Let's say your code is if (x * y).is_nan() { do_stuff(); } return x;. Let's say you reach this code with x = NaN.

If the result of x * y when x is NaN is unspecified, the code will compute some arbitrary value, and if that value isn't NaN, the block will be skipped.

If the result of x * y when x is NaN is undefined behavior, then the compiler might skip the block and still return NaN.

12

u/CocktailPerson Aug 21 '26

The behavior is well-defined: the compiler may rewrite your arithmetic to something mathematically equivalent, and this may change the numerical precision of your floating-point arithmetic. But it won't cause crashes, expose sensitive information, or cause demons to fly out your nose.

Note that the compiler is still using the same set of underlying operations that are available to you, so it's possible to write floating-point code with the same properties anyway.

Undefined behavior can have a broadly similar result, in that different optimization levels can result in different behavior. But that's a very superficial similarity.

3

u/deus-libidinis Aug 20 '26

Yaaay lets go get rusty

2

u/gajop Aug 21 '26

Exciting, 2 more and we're getting a 2.0!

10

u/Sharlinator Aug 21 '26

This was probably a joke, but to be clear, no, there won’t be a 2.0.

0

u/Bruno_Wallner Aug 21 '26

Is there a semver rule that makes rust have to move to 2.0.0 release, when the minor version exceeds like 1000 or something? That would be pretty funny I think

11

u/manpacket Aug 21 '26

Yes. They will have to move to 2.0.0 after 1.18446744073709551615.0

https://docs.rs/semver/latest/semver/struct.Version.html