Clang 14 和 15 显然优化了在 Clang 13、ICC、GCC、MSVC 下按预期编译的代码

Iam*_*mIC 2 c++ clang strict-aliasing compiler-optimization type-punning

我有以下示例代码:

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);
}
Run Code Online (Sandbox Code Playgroud)

在 下x86-64 clang 13.0.1,代码按预期编译。

在 or 15下x86-64 clang 14.0.0,输出仅仅是andret的运算。next1(float)next2(float)

编译器选项:-march=x86-64-v3 -O3

代码和输出在这里:Godbolt

successor(float,bool)函数不是空操作。

请注意,输出符合 GCC、ICC 和 MSVCC 下的预期。我在这里错过了什么吗?

use*_*522 14

*(unsigned long int*)&f是立即的别名违规。f是一个float. 不允许您通过指向 的指针访问它unsigned long int。(这同样适用于*(float*)&i。)

因此,代码具有未定义的行为,并且 Clang 喜欢假设具有未定义行为的代码是不可访问的。

编译以-fno-strict-aliasing强制 Clang 不将别名冲突视为不可能发生的未定义行为(尽管这可能还不够,请参见下文),或者最好不要依赖未定义行为。而是使用std::bit_cast(C++20 起) 或创建具有新类型但具有相同对象表示形式的std::memcpy副本。f这样您的程序将是有效的标准 C++,而不依赖于-fno-strict-aliasing编译器扩展。

(如果您使用std::memcpyadd astatic_assert来验证这一点unsigned long intfloat具有相同的大小。这在所有平台上都不是这样,也不是在所有常见平台上都是如此。std::bit_cast内置了测试。)


正如 @CarstenS 在另一个答案中注意到的,考虑到您(至少在编译器资源管理器上)正在为 SysV ABI 进行编译, (64 位)确实与(32 位)unsigned long int大小不同。float因此,存在更直接的 UB,因为您在 的初始化中访问内存越界i。他还注意到,当使用匹配大小的整数类型时,即使没有-fno-strict-aliasing. 不过,这并不会使我上面写的内容无效。