How does Rust/x86_64 handle underflow/overflow?
- ignores it => this seems dangerous
- check on every op => this seems expensive
- register some type of handler in x86_64 ? => is this even possible
How does Rust/x86_64 handle underflow/overflow?
Do you mean integer overflow?
It's covered by The Book:
Let’s say you have a variable of type
u8that can hold values between 0 and 255. If you try to change the variable to a value outside of that range, such as 256, integer overflow will occur. Rust has some interesting rules involving this behavior. When you’re compiling in debug mode, Rust includes checks for integer overflow that cause your program to panic at runtime if this behavior occurs. Rust uses the term panicking when a program exits with an error; we’ll discuss panics in more depth in the “Unrecoverable Errors with
panic!” section in Chapter 9.
When you’re compiling in release mode with the
--releaseflag, Rust does not include checks for integer overflow that cause panics. Instead, if overflow occurs, Rust performs two’s complement wrapping . In short, values greater than the maximum value the type can hold “wrap around” to the minimum of the values the type can hold. In the case of a
u8, 256 becomes 0, 257 becomes 1, and so on. The program won’t panic, but the variable will have a value that probably isn’t what you were expecting it to have. Relying on integer overflow’s wrapping behavior is considered an error.
To explicitly handle the possibility of overflow, you can use these families of methods that the standard library provides on primitive numeric types:
- Wrap in all modes with the
wrapping_*methods, such as
- Return the
Nonevalue if there is overflow with the
- Return the value and a boolean indicating whether there was overflow with the
- Saturate at the value's minimum or maximum values with
Not as much as you may think. The branch predictor nornally predicts that the overflow branch is not taken, which reduces overhead on an average program to less than a couple of percents.
What I would like to see is a way to have panics on overflow when building in release mode.
To my mind overflowing a number range is a memory misuse error. Rust is fanatical about preventing memory misuse errors. Why not this one?
Now bear with me a moment....
When one writes something like
someArray[x] = 42 the index
x is checked and if it falls outside the size of the array there is a panic.
Why? Because allowing access outside the array limits would be a memory misuse error.
So far so good.
My claim is that when one writes
x = x + 1 and that causes the number range of
x to be exceeded that is also a memory misuse error. What one is asking for there is for another bit to be used to hold the result. The memory for that is not available. Ergo a panic should result, for the same reason as array bounds violation.
This hole in the correctness of our computing systems is so universal and been with us so long, all the way down to the hardware, that we just ignore it. Papering over it by redefining
+ and that like as modulo operations.
Which is fair enough. Rust deals with
i32 etc. and defines the operations on them as it likes. It's unforgivable in languages that have types like
int that are not actually integers and have undefined behaviours on overflow.
I think you can already
overflow-checks = true in Cargo.toml or something like that
Excellent. Seems rustc has a flag to do this '-Z force-overflow-checks'.
I have no idea how or if that can be set from Cargo.
But then isn't there the problem that forcing overflow checks across the entire build could break some crate that relies on wrapping behaviour?
An interesting look at this whole issue is here:
iirc crates that rely on overflow behaviour without specifying it explicitly (i.e. with some kind of
.add_overflow function) are already broken in debug mode, no?
Is it possible to maybe specify rustc flags only to the root crate?
Yes, sorry for not being clear, I am referring to things like:
u32 + u32 w result > what u32 can represent u64 + u64 w result > what u64 can represent i32 + i32 w result > what i32 can represent i64 + i64 w result > what i64 can represent
Interesting. My intuition is that these checks would make each program twice as long, which screws up the L1 instruction cache. I am surprised this only hurts performance by a few %.
I am not very familiar with x86_64. How do these panics get triggered? Are we (1) registering some handler that gets called on overflow or (2) is Rust inserting code that checks flags after every +-*/ ?
I think so.
That will only get caught in debug mode if there are tests in place for it.
It's number 2, the processor sets a flag if the operation results in a overflow, and it's up to the program to check it and handle it (Or simply ignore it)
When I first read about this, I thought to myself what an odd choice.
I can't think of a case where I would want different behavior in debug mode or release mode intentionally, and overflow checks is really no exception. It seems an invite for subtle bugs and developer confusion. It smells of past mistakes such as gcc requiring an optimization level greater than 0 to find uninitialized variables, etc. What was the rationale for this choice?
I wonder though whether Rust's powerful macro facilities couldn't help here. Is it possible for instance to write a macro
checked! (akin to C#'s checked keyword):
checked!( // arbitrary code here where the `+` operator is turned into `checked_` + `unwrap()` or something like that )
The problem is not just that extra (never taken) branch, it can also prevent autovectorization, reordering and merging instructions, which can have far bigger impact than a never taken branch.
The idea is that you do your tests in debug mode where overflow checks are on and can catch potential bugs, but in release you want speed, thus the overflow checks are removed.
This is actually the opposite of what's happening here, in the gcc case the bugs will be hidden until you compile in release mode, so they will be missed during tests, while in rust's case they can only be catched during tests.
Debug mode compiles with no optimizations, which means the code is way too slow to test large test input sizes. But large test input sizes tend to be the ones that cause overflows.
Overflow checks have definitely caught bugs for me. There are lots of cases where testing in debug mode is fine.
One instance I was thinking of is competitive programming, where problem setters tend to include large input sets for instance in shortest-path problems that trigger 32-bit integer overflow. It would be really nice to have a way to turn on Rust's overflow checking while also running with optimizations on (and without the awkward explicit function use).
Are you looking for Rust / x86_64, underflow / overflow - #5 by Gilnaa ?
No, unfortunately, it has to be in the source code (and needs to be in stable).
These are the rustc options used.
Is the following correct: ?