TL;DR: please profile/benchmark your code on
nightly-2017-11-12 or later with and without
-Zsaturating-float-casts so we can make an informed decision! We are hoping to enable this flag by default in future versions to plug a long-standing soundness hole!
Since long before Rust 1.0, float->int casts have had a soundness hole: if the input value is larger or smaller than the target type can hold (or NaN), the result is Undefined Behaviour. This is because we just lower these casts to LLVM intrinsics, and that’s what those intrinsics say.
-1.0 as u8,
300.0 as i8,
NaN as u32, and
f32::INFINITY as u128 are all currently UB in Rust.
The reason this took so long to fix is that, well, we didn’t know what to do in this case! After some long discussion (which you can find in the
float->int issue) saturating appeared to be the most reasonable solution.
Specifically, these behaviours would be guaranteed at runtime (note: small means “very negative”):
TOO_LARGE_FLOAT as int == int::MAX
TOO_SMALL_FLOAT as int == int::MIN
NaN as int == 0
At compile-time (
const fn), these casts are currently errors. This is the most conservative thing to do while the runtime semantics are being decided, eventually constant evaluation should match runtime. Constant evaluation is not affected by the
-Z saturating-float-casts flag because it is not performance critical.
You can see these two tests for a detailed enumeration of interesting cases.
The arguments in favour of this behaviour are that:
- It matches the spirit of floats, which favour saturating to ±Infinity.
- Creates the closest possible value, and least “random” value.
- Matches the behaviour of ARM’s
vcvt instruction, making this have no overhead there.
- Should optimize reasonably well on other platforms.
Panicking at runtime for these cases was considered unacceptable for two reasons:
- Early testing found it to be very slow.
- All other casts currently always succeed (
int -> int wraps while
float -> float and
int -> float saturates to ±Infinity).
Wrapping at runtime was considered to be a bit too random (
int -> int wrapping is traditional, has a simple/useful bitwise interpretation, and is very uniformly supported).
Testing It Out
Behaviourally, saturating is technically backwards-compatible because applications relying on this behaviour were relying on Undefined Behaviour. However we take performance seriously, and we want to understand the performance impact of this change before moving forward and putting this change through the proper RFC process. There were some preliminary measurements of this change, but we found the performance impact to be very workload-specific. As such, we’d be interested in recieving community feedback!
The proposed solution has been merged under the unstable, nightly-only
-Zsaturating-float-casts flag. You will need the nightly from 2017-11-12 or newer, as earlier nightlies either don’t have the flag, or have a prelimary version that does more work than necessary (which could affect measurements). If your applications or libraries have any benchmarks, we’d be interested in seeing how the results compare when built with and without
> cargo +nightly bench
> RUSTFLAGS="-Zsaturating-float-casts" cargo +nightly bench
… and then post the results here.
Elsewhere In Undefined Casts
The sibling of this issue is the
u128 -> f32 cast having Undefined Behaviour because u128 can exceed the maximum finite value of f32. This was recently fixed by saturating (at runtime as well as at compile time). Saturating matches how out of bounds float literals behave, and is compliant with IEEE-754. For further details, see issue #41799 and the PR that fixed it.