Join the discussion

Write your take first — we'll ask for email only when you're ready to publish.

  • Hacker News
  • Ideally CPython would have a JIT that would be able to do this, depending on the current hardware like other ecosystems, but we are still not there yet.
  • RustPython?
  • CPython can't do this because it's a change in semantics. You need explicit opt-in from the programmer.

    Anyway adding this optimisation to CPython would be like putting active aero on a dandy horse.

  • I'm not an expert on floating point math, but the "Does adding a small number do nothing?" example caught my eye, because the numbers used are constants, which are arbitrary precision in some languages (https://stackoverflow.com/questions/57511935/what-is-the-pur...). For instance, Go answers the question with false (but still prints out "1e+16" when you try to print 1e16 + 1): https://go.dev/play/p/mSAktWpCRJA
  • Rust explicitly requires that constants are typed.

        const FOO: f32 = 0.75; // The 32-bit floating point value three quarters
    
    If you try

        const UNTYPED = 0.75; // Does not compile, pick a type
    
    I don't find the SO answer very convincing because it seems like it's trying to argue this is the Reals, and it just isn't, it's only a subset of the Rationals which happened to be convenient for Go to work with it. The Reals are much stranger.
  • > Floating point math is often slower than integer math because the compiler is being conservative about how it optimizes your code.

    It's not strictly true to say that it's "being conservative". What is more correct is to say that floating point operations have different semantics to integer operations, and an optimisation that retains the semantics of an expression over integers may not do so when applied to an expression over integers. Hence, it may be possible to apply one optimisation to an integer expression, but applying that to a floating-point expression may result in a different program meaning.

    C/C++ compilers give you a way out of this with the `--ffast-math` flag, which essentially allows compilers to relax the constraints on floating-point optimisation passes.

    For an example of how this works in GCC, take a look here: https://gcc.gnu.org/wiki/FloatingPointMath

  • > C/C++ compilers give you a way out of this with the -fast-math

    People should really use the individual optimization flags they want (no signed zeros, no trapping math, associative math, reciprocal math) and not -ffast-math because the other optimizations it enables leads to surprising code (for example, isinf and isnan may become noops, which will break production code).

    Basically never use an optimization flag that changes the semantics of your code without understanding exactly what that means. I have had to fix this in a number of codebases because someone thought that flag was as innocuous as -O3.

    And if you have to enable flush to zero/denormals are zero it should be explicit in your code and scoped.

  • I like the idea, but I hate how verbose it is. Would be nice if there was also a macro that would transform all arithmetic within a block into algebraic arithmetic. The name is also a bit misleading, as I would expect "alebraic_add" to give an exact algebraic result, but instead it enables optimisations based on associative semantics.

    As a sidenote, if implemented as a macro, e.g.

        fn fast_sum_f64(values: &[f64]) -> f64 {
            let mut total = 0;
            fp_opt!(associative, {
                for value in values {
                    total += value;
                }
            });
            return total;
        }
    
    A question arises what happens when operating on custom types that overload arithmetic operations. I think the cleanest approach here would be to let the custom type define optimised versions, or have another macro that automatically generates them based on the existing ones, i.e. propagate the optimisation flags.
  • You can create an `struct Algebraic<T>(pub T)` newtype that overloads operators using algebraic methods. In fact such wrapper might be added to std (it was discussed but decided that not yet. Such wrappers exist for wrapping and saturating arithmetic).
  • Signed integer addition is only associative when overflow is defined to wrap around like unsigned arithmetic. This condition is matched here, because only debug builds panic on overflow. However, it's a bit of a gray area that the article completely ignores.
  • Unlike C, Rust's signed arithmetic is fully specified to wrap around.