Is fast-math supported with juce?

As of Xcode 16.3 it’s showing a lot of warnings. Is it safe to ignore:

warning: use of infinity is undefined behavior due to the currently enabled

1 Like

Because of this, I have changed flags to:

if (MSVC)
    # fast math and better simd support in RELEASE
    # https://learn.microsoft.com/en-us/cpp/build/reference/fp-specify-floating-point-behavior?view=msvc-170#fast
    target_compile_options(SharedCode INTERFACE $<$<CONFIG:RELEASE>:/fp:precise>)
else ()
    # See the implications here:
    # https://stackoverflow.com/q/45685487
    target_compile_options(SharedCode INTERFACE $<$<CONFIG:RELEASE>:-O3 -fno-signed-zeros -freciprocal-math -ffp-contract=fast>)
    target_compile_options(SharedCode INTERFACE $<$<CONFIG:RelWithDebInfo>:-O3 -fno-signed-zeros -freciprocal-math -ffp-contract=fast>)
endif ()

No! -ffast-math is unsafe, and should almost never be used. Prefer the more granular fp optimization flags instead.

Notably, -ffinite-math-only is implied by -ffast-math which means that any code relying on checks for inf or NaN are going to be optimized away.

1 Like

I’m all for Health and Safety in the Juce framework. Especially if you’re new to C++. In my book, DSP needs to be fast and tight, with as much SIMD as possible. Keep the choice in there, please. :kissing_face_with_closed_eyes:

1 Like

I second this. We really rely on fast-math for auto-vectorisation and it makes a huge difference.

It’s nice that there are now compiler warnings flagging unsafe functions but they should be updated to safe operations rather than saying disable fast-math.

9 Likes

There is a related problem that I’d like to bring up here again: If -ffast-math or -Ofast are used and inf/nan detection no longer works (with recent versions of XCode), a plugin can no longer detect inf/nan on incoming audio streams using the usual functions like fpclassify/isnan/isinf.

Hand-rolled variants need to be used in order to guard against invalid values that might come from previous plugins, loaded sample files or even the hosts.

Here’s the previous thread about this issue:

Inf/nan detection has never worked with -ffast-math, it’s not a recent Xcode thing.

No, it worked perfectly fine up to XCode 12, but it might also depend on the std lib used.

You can enable the optimizations that improve autovectorization in your DSP without breaking every line that uses std::isinf or std::isnan, or replacing each of those calls with a reimplementation.

This also isn’t super uncommon in my own dsp - -inf is a pretty common bit pattern for gain values…

1 Like

Theoretically you can control compile flags per translation units (and there is also #pragma STDC FENV stuff but last I checked MSVC doesn’t support it), but ofc you can’t rely on this for any code in headers

One thing I wonder: under fast math, are compilers allowed to detect custom implementations of isinf/isnan and also replace those calls with false, or is this only allowed for std::isinf/std::isnan?

1 Like

Yes, I know there are more fine-grained controls over floating point maths optimisations and auto-vectorisation, it’s just that -ffast-math is probably the most commonly used.
I was more saying that a library like juce should support those options and work correctly with them enabled, otherwise you’re forcing that choice upon the user.
If you want to use inf/nan, or not, in your own code that should be up to you.

I know there is the ability to set flags per-translation unit but this isn’t often used and complicates things. For example, what if a juce class produces a nan/inf but then you can’t detect it? Sanitising plugin hosting for code you didn’t compile is a common thing for hosts but sanitising your own code everywhere because it could be compiled with different flags seems like a headache.

3 Likes

Yep, I’d slaps like proper fast math support. We’ve been shopping software built with fast-math for a while and it’d be good to know the library was built with this in mind, and any limitations documented.

Hey just bumping this as it’d be great to get this sorted :slight_smile:


/**
 * Tests to determine that the fast math usage isn't break our basic usage of infinity.
 */
class FastMathTests
{
public:
    static void run()
    {
#if DM_FAST_MATH_ENABLED
        FastMathTests tests;
        tests.performFloatTests();
        tests.performDoubleTests();

        if (! tests.failedTests.isEmpty())
        {
            jcf_error ("FAST MATH TESTS FAILED//\n\n" + tests.failedTests.joinIntoString ("\n"));
        }
#else
        jassertfalse;
        // fast math isn't enabled?
#endif
    }

private:
    void performFloatTests()
    {
        const float inf = std::numeric_limits<float>::infinity();
        const float ninf = -std::numeric_limits<float>::infinity();

        // sanity
        test (std::numeric_limits<float>::has_infinity, "float has_infinity == false");

        // comparisons & propagation
        test (inf > 1e20f, "float: +inf > large failed");
        test (inf + 1e20f == inf, "float: +inf + finite != +inf");
        test (inf + inf == inf, "float: +inf + +inf != +inf");
        test ((std::isnan) (inf - inf), "float: +inf - +inf should be NaN");
        test (inf * 2.0f == inf, "float: +inf * 2 != +inf");
        test (inf / 2.0f == inf, "float: +inf / 2 != +inf");
        test ((std::isnan) (inf * 0.0f), "float: +inf * 0 should be NaN");
        test ((std::isnan) (inf / inf), "float: +inf / +inf should be NaN");

        // divide by zeros (IEEE-754)
        const float pz = 0.0f; // +0
        const float nz = -0.0f; // -0
        test ((std::isinf) (1.0f / pz) && ! std::signbit (1.0f / pz), "float: 1/+0 != +inf");
        test ((std::isinf) (1.0f / nz) && std::signbit (1.0f / nz), "float: 1/-0 != -inf");
        test ((std::isinf) (-1.0f / pz) && std::signbit (-1.0f / pz), "float: -1/+0 != -inf");
        test ((std::isinf) (-1.0f / nz) && ! std::signbit (-1.0f / nz), "float: -1/-0 != +inf");

        // isinf/signbit
        test ((std::isinf) (inf) && ! std::signbit (inf), "float: +inf isinf/signbit failed");
        test ((std::isinf) (ninf) && std::signbit (ninf), "float: -inf isinf/signbit failed");

        // bit patterns (IEEE-754 single)
        const std::uint32_t posInfBits = 0x7F800000u;
        const std::uint32_t negInfBits = 0xFF800000u;

        const auto infAsInt = std::bit_cast<std::uint32_t> (inf);
        const auto ninfAsInt = std::bit_cast<std::uint32_t> (ninf);
        test (infAsInt == posInfBits, "float: +inf bit pattern incorrect");
        test (ninfAsInt == negInfBits, "float: -inf bit pattern incorrect");

        const auto infFromBits = std::bit_cast<float> (posInfBits);
        const auto ninfFromBits = std::bit_cast<float> (negInfBits);
        test (infFromBits == inf, "float: +inf from bits incorrect");
        test (ninfFromBits == ninf, "float: -inf from bits incorrect");

        // overflow to infinity
        test (std::numeric_limits<float>::max() * 2.0f == inf, "float: overflow should be +inf");

        // dB -> gain: -inf dB => 0 (keep if you have this utility)
        test (Decibels::decibelsToGain (-std::numeric_limits<float>::infinity()) == 0.0f, "float: decibelsToGain(-inf) should be 0");

        // NaN basics
        test (std::log (-1.0f) != std::log (-1.0f), "float: NaN != NaN failed");
        test ((std::isnan) (std::log (-1.0f)), "float: log(-1) should be NaN");
    }

    void performDoubleTests()
    {
        const double inf = std::numeric_limits<double>::infinity();
        const double ninf = -std::numeric_limits<double>::infinity();

        // sanity
        test (std::numeric_limits<double>::has_infinity, "double has_infinity == false");

        // comparisons & propagation
        test (inf > 1e300, "double: +inf > large failed");
        test (inf + 1e300 == inf, "double: +inf + finite != +inf");
        test (inf + inf == inf, "double: +inf + +inf != +inf");
        test ((std::isnan) (inf - inf), "double: +inf - +inf should be NaN");
        test (inf * 2.0 == inf, "double: +inf * 2 != +inf");
        test (inf / 2.0 == inf, "double: +inf / 2 != +inf");
        test ((std::isnan) (inf * 0.0), "double: +inf * 0 should be NaN");
        test ((std::isnan) (inf / inf), "double: +inf / +inf should be NaN");

        // divide by zeros (IEEE-754)
        const double pz = 0.0; // +0
        const double nz = -0.0; // -0
        test ((std::isinf) (1.0 / pz) && ! std::signbit (1.0 / pz), "double: 1/+0 != +inf");
        test ((std::isinf) (1.0 / nz) && std::signbit (1.0 / nz), "double: 1/-0 != -inf");
        test ((std::isinf) (-1.0 / pz) && std::signbit (-1.0 / pz), "double: -1/+0 != -inf");
        test ((std::isinf) (-1.0 / nz) && ! std::signbit (-1.0 / nz), "double: -1/-0 != +inf");

        // isinf/signbit
        test ((std::isinf) (inf) && ! std::signbit (inf), "double: +inf isinf/signbit failed");
        test ((std::isinf) (ninf) && std::signbit (ninf), "double: -inf isinf/signbit failed");

        // bit patterns (IEEE-754 double)
        const std::uint64_t posInfBits = 0x7FF0000000000000ull;
        const std::uint64_t negInfBits = 0xFFF0000000000000ull;

        const auto infAsInt = std::bit_cast<std::uint64_t> (inf);
        const auto ninfAsInt = std::bit_cast<std::uint64_t> (ninf);
        test (infAsInt == posInfBits, "double: +inf bit pattern incorrect");
        test (ninfAsInt == negInfBits, "double: -inf bit pattern incorrect");

        const auto infFromBits = std::bit_cast<double> (posInfBits);
        const auto ninfFromBits = std::bit_cast<double> (negInfBits);
        test (infFromBits == inf, "double: +inf from bits incorrect");
        test (ninfFromBits == ninf, "double: -inf from bits incorrect");

        // overflow to infinity
        test (std::numeric_limits<double>::max() * 2.0 == inf, "double: overflow should be +inf");

        // NaN basics
        test (std::log (-1.0) != std::log (-1.0), "double: NaN != NaN failed");
        test ((std::isnan) (std::log (-1.0)), "double: log(-1) should be NaN");
    }

    void test (bool condition, String message)
    {
        if (! condition)
        {
            failedTests.add (message);
        }
        else
        {
            passedTests.add (message);
        }
    }

    StringArray passedTests;
    StringArray failedTests;
};

So this set of tests seem to pass entirely on clang with fast-math enabled, and all but the nan != nan tests pass on Windows with /fp:fast.

So I’m thinking the warning for the double in CharPointer could probably just be suppressed?

2 Likes