Attribute for specifying floating-point fast-math flags

The _fast functions (fadd_fast, fmul_fast,...) on std::intrinsics seem insufficient because they

  • enable all the fast-math flags and don't allow specifying just one or a subset of the flags
  • don't work on SIMD functions
  • require rewriting code to call them to take advantage of the optimizations

Hence why I think a fast-math attribute is necessary (made a prototype here). I'll draft a pre-RFC if this feature is acceptable to be included in Rust.

This has come up many times; here's one I found from a couple months ago: Suggestion: FFastMath intrinsics in stable - #3 by scottmcm

I strongly believe that crate-level is inappropriate for this. Even function-level, because that composes poorly with things like .sqrt() which is a method, not an operator, so there's no nice way to get the fast math flags to apply to it.

That said, there's certainly interest in the area, and I'd like to have something for it. I think it needs someone to charter a lang/libs-supervized initiative to figure out the best way to expose it, though. See the portable simd work for potential inspiration.

May I ask why? Something about unsafe-ness? I was hoping that if the function attribute was not accepted, the crate attribute would still be because it's like ~16 lines to implement and doesn't have any ugly hacks.

My problem implementing this wasn't that .sqrt() being a method - the flag can still easily be flipped within the LLVM IR - but telling that it's an f32 method that would produce an LLVM intrinsic that can have fast-math flags.

With that prototype (which refuses to build on the CI's LLVM 10), something like

#![feature(unsafe_fp_math)]

#[unsafe_fp_math(enable = "all")]
pub fn sqrt(x: f32) -> f32 {
    // CHECK-LABEL: {{_ZN.+7sqrtf32}}
    // CHECK: call fast float @llvm.sqrt.f32
    x.sqrt()
}

appropriately produces

; foo::sqrtf32
; Function Attrs: nounwind readnone uwtable willreturn
define float @_ZN3foo7sqrtf3217h75e46858cdbdf847E(float %x) unnamed_addr #0 !rust.unsafe-fp-math.flags !2 {
start:
  %0 = tail call fast float @llvm.sqrt.f32(float %x)
  ret float %0
}

I'm using an ugly regex but I reckon it won't be as hacky if we can annotate stdlib floating-point functions with an #[inherits_unsafe_fp_math] internal attribute. Not sure how much of a compilation time hit is annotating a bunch of functions with LLVM metadata is though. I was trying to avoid any perf cost for non-users of the fast-math attribute.

I guess I'll just use this internally then.

I believe so for the same reason why we have unsafe {} block instead of unsafe = true attribute on the Cargo.toml. All the fast math operations are unsafe - especially if they're operate on raw f32/f64 types directly. Implementation complexity can only be the concern if you successfully justified the feature itself.

Isn't that just a matter of requiring functions of the module crate to be unsafe? I mean LLVM handles the expressions composed of different fast-math flags by favoring the stricter one so FMF won't necessarily "leak out" of the unsafe function.

EDIT: module -> crate

Not sure how feasible this is, but maybe we could (ab)use the unsafe keyword (toying with syntax ideas):

#[fastfloat]
unsafe {
    let a = 1.0 * 2.0;
}

// or maybe

unsafe(fastfloat) {
    let b = 3.0 * 4.0;
}

Similar to a normal unsafe block, this would not apply transitively (so calling a function which uses floats would not be affected by this outer unsafe).

But I guess the "not transitive" part falls apart as stated in the linked post (because how would f64::sqrt() do fast math, with it being a function?).

Maybe downstream crates should have a way to "opt-in" to say "yes, I can be used with fast floats" (as it needs to be safe by default; but then again the user has already opted into the unsafe so maybe it would be enough if the crates documented their safety regarding floats?).

Since f32/f64 are defined as IEEE 754-2008 floating point types, I believe arithmetic operators on them should always follow the IEEE754 operations. But it would be ok to provide fast math semantics on operations over some wrapper type, if they can only be evaluated within the unsafe context like dereferencing raw pointers. This also solves method problems like .sqrt() as the wrapper type can provides its own implementation of it with fast math semantics.

use std::num::FastMath;

let (a, b) = (3.0, 4.0);
let (c, d) = unsafe {
    let ieee_754 = a + b;
    let fast_math = FastMath(a) + FastMath(b);
    (ieee_754, fast_math)
};
let e = FastMath(a) + FastMath(b); // compile error

But since we don't have any unsafe arithmetics yet, implementation would not be trivial. Also having it under nightly feature only prevents many users to have experiment with it. Instead, if the intrinsics themselves are available on stable, anything other can be implemented as a 3rd party crate with some macro and trait tricks.

use fast_math_helper::{fast_math, FastMath};

let (a, b) = (3.0, 4.0);
#[fast_math]
let (c, d) = unsafe {
    let ieee_754 = a + b;
    let fast_math = FastMath(a) + FastMath(b);
    (ieee_754, fast_math)
};
let e = FastMath(a) + FastMath(b); // compile error
2 Likes