← Back to context

Comment by athrowaway3z

5 hours ago

Last time i checked; no.

But I suspect you're overvaluing the potential savings. Knowing when a float is 0.0 or NaN beforehand is almost entirely impossible, except for the most trivial of cases - like when you first initialize a variable or first enter a loop. Everything after that is very hard or impossible with floats as they are.

Those cases can be const folded at compile time.

Those cases are never a measurable bottleneck.

The closest thing I know of in the realm of the optimization you're curious about is Rust NonZero* variants, but they're used for enum compression afaik.

I’m not sure I agree on the impossible part, I feel like a sufficiently smart interprocedural analysis that also implements range analysis interprocedurally could prove a lot to where it becomes useful.

I guess what I would like to see is SIMD libraries being able to confidently say nobody needs to use intrinsics (or differentiate between relaxed/normal SIMD on the user API level) because the language + high level SIMD APIs are smart enough to choose the right implementation.

IIRC IEEE min/max with proper NaN handling needs 8 instructions on x86 vs 1 on arm64 I find it very sad that we apparently haven’t really solved that yet without forcing the user to use different APIs.

  • Anything without range analysis is not worth it.

    Note that:

    NonZerof32 * NonZerof32 -> NonNanf32

    NonZerof32::from_bits(1) multiplied with itself is zero.

    Doing range analysis needs the language to support it at compile time, and the dev to specify what range it is.

    The only 'stable' thing i can think of is a type for 'greater-eq-one' using only addition and multiplication. Practically every other operation breaks most of the type knowledge up to that point.

  • You can do it with just 3 instructions for IEEE 754-2019 minimumNumber (ignores NaN):

            vminpd          ymm2, ymm1, ymm0
            vcmpunordpd     ymm0, ymm0, ymm0
            vblendvpd       ymm0, ymm2, ymm1, ymm0
    

    If you want proper IEEE 754-2019 minimum (propagate NaN, -0.0 < +0.0, NaN bitpattern picked in the usual way) you can do it in 6:

            vminpd          ymm1, ymm0, ymm1
            vbroadcastsd    ymm2, qword ptr [rip + .LCPI0_0]
            vandpd          ymm2, ymm0, ymm2
            vorpd           ymm1, ymm2, ymm1
            vcmpunordpd     ymm2, ymm0, ymm0
            vblendvpd       ymm0, ymm1, ymm0, ymm2
    

    I personally find this a load of nonsense I don't care about.

    If you want propagating NaNs but don't care about signed zero or NaN payload/sign, you can use

            vminpd  ymm2, ymm0, ymm1
            vminpd  ymm1, ymm1, ymm0
            vorpd   ymm0, ymm1, ymm2
    

    What I do in Polars is a bit different, there for propagating NaNs I do

        if (self < other) | self.is_nan() { self } else { other}
    

    this isn't fully optimal on x86-64 but it's fairly simple and autovectorizes decently on various platforms, here's AVX2:

            vcmpltpd        ymm2, ymm0, ymm1
            vcmpunordpd     ymm3, ymm0, ymm0
            vorpd           ymm2, ymm3, ymm2
            vblendvpd       ymm0, ymm1, ymm0, ymm2