Search code examples
c++clangcompiler-optimizationstrict-aliasingtype-punning

Clang 14 and 15 apparently optimizing away code that compiles as expected under Clang 13, ICC, GCC, MSVC


I have the following sample code:

inline float successor(float f, bool const check)
{
    const unsigned long int mask = 0x7f800000U;
    unsigned long int i = *(unsigned long int*)&f;

    if (check)
    {
        if ((i & mask) == mask)
            return f;
    }

    i++;

    return *(float*)&i;
}

float next1(float a)
{
    return successor(a, true);
}

float next2(float a)
{
    return successor(a, false);
}

Under x86-64 clang 13.0.1, the code compiles as expected.

Under x86-64 clang 14.0.0 or 15, the output is merely a ret op for next1(float) and next2(float).

Compiler options: -march=x86-64-v3 -O3

The code and output are here: Godbolt.

The successor(float,bool) function is not a no-op.

As a note, the output is as expected under GCC, ICC, and MSVCC. Am I missing something here?


Solution

  • *(unsigned long int*)&f is an immediate aliasing violation. f is a float. You are not allowed to access it through a pointer to unsigned long int. (And the same applies to *(float*)&i.)

    So the code has undefined behavior and Clang likes to assume that code with undefined behavior is unreachable.

    Compile with -fno-strict-aliasing to force Clang to not consider aliasing violations as undefined behavior that cannot happen (although that is probably not sufficient here, see below) or better do not rely on undefined behavior. Instead use either std::bit_cast (since C++20) or std::memcpy to create a copy of f with the new type but same object representation. That way your program will be valid standard C++ and not rely on the -fno-strict-aliasing compiler extension.

    (And if you use std::memcpy add a static_assert to verify that unsigned long int and float have the same size. That is not true on all platforms and also not on all common platforms. std::bit_cast has the test built-in.)


    As noticed by @CarstenS in the other answer, given that you are (at least on compiler explorer) compiling for the SysV ABI, unsigned long int (64bit) is indeed a different size than float (32bit). Consequently there is much more direct UB in that you are accessing memory out-of-bounds in the initialization of i. And as he also noticed Clang does seem to compile the code as intended when an integer type of matching size is used, even without -fno-strict-aliasing. This does not invalidate what I wrote above in general though.