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!
Background
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.
For instance, -1.0 as u8
, 300.0 as i8
, NaN as u32
, and f32::INFINITY as u128
are all currently UB in Rust.
Proposed Solution
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 whilefloat -> float
andint -> 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 -Zsaturating-float-casts
, e.g.:
> 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.