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.

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 -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