C++ 编译器为 AVX SIMD 代码中从自身减去 +-Infinity 或 +-NaN 的恒定传播给出不同的 NaN 符号

标签 c++ gcc clang nan avx

我正在研究如何检测 SIMD 寄存器的哪些 channel 中的 float 是 +/- 无穷大或 +/- nan。在运行时看到一些奇怪的行为后,我决定将东西扔到 Godbolt 中进行调查,事情很奇怪: https://godbolt.org/z/TdnrK8rqd

#include <immintrin.h>
#include <cstdio>
#include <limits>
#include <cstdint>

static constexpr float inf = std::numeric_limits<float>::infinity();
static constexpr float qnan = std::numeric_limits<float>::quiet_NaN();
static constexpr float snan = std::numeric_limits<float>::signaling_NaN();

int main() {
    __m256 a = _mm256_setr_ps(0.0f, 1.0f, inf, -inf, qnan, -qnan, snan, -snan);

    __m256 mask = _mm256_sub_ps(a, a);

    // Extract masks as integers 
    int mask_bits = _mm256_movemask_ps(mask);

    std::printf("Mask for INFINITY or NaN: 0x%x\n", mask_bits);

    #define PRINT_ALL
    #ifdef PRINT_ALL
    float data_field[8];
    float mask_field[8];
    _mm256_storeu_ps(data_field, a);
    _mm256_storeu_ps(mask_field, mask);
    for (int i = 0; i < 8; ++i) {
        std::printf("isfinite(%f) = %x = %f\n", data_field[i], ((int32_t*)(char*)mask_field)[i], mask_field[i]);
    }
    #endif
    
    return 0;
}

编译器给出不同的结果,甚至根据优化级别产生不同的结果。一些编译器只是在编译时通过(损坏的?)推理完全执行代码,并且全部编译为一些硬编码的打印语句,而在运行时没有实际计算。更改优化级别会导致某些编译器触发此(不正确?)优化?

此外,我似乎还通过手动打印所有结果(PRINT_ALL 选项)来影响所发生的情况。

打印的掩模差异很大:

  • 没有PRINT_ALL:
    • GCC 13.1 -O0:0xac - 新的 NaN 为 -nan;保留输入 NaN 的符号。
    • GCC 13.1 -O1:0x5c - 新的 NaN 为 -nan,翻转输入 NaN 的符号。
    • Clang 16.0.0 -O0:0xac
    • Clang 16.0.0 -O1:0xa0 - 新的 NaN 为 +nan;保留输入 NaN 的符号。
    • ICX 2022.2.1 -O0:0xac
    • ICX 2022.2.1 -O1:0xa0
  • 使用 PRINT_ALL,优化的 GCC 现在可以匹配硬件的功能,LLVM(clang 和 ICX)不会改变。
    • GCC 13.1 -O0:0xac
    • GCC 13.1 -O1:0xac
    • Clang 16.0.0 -O0:0xac
    • Clang 16.0.0 -O1:0xa0
    • ICX 2022.2.1 -O0:0xac
    • ICX 2022.2.1 -O1:0xa0

“新 NaN​​”是 inf - inf-inf - -inf,其中结果为 NaN,但两个输入都不是 NaN。它们构成低十六进制数字的高 2 位,即 0x?C0x?0。该半字节的低 2 位来自 0-01-1 元素,它们产生 +0.0 输出 as required对于有限same-same,舍入模式不是 -Inf(这不是默认值。)

ICX 和 clang 似乎彼此一致,但根据优化级别的不同,结果仍然不同。我猜测 0xac 是正确的结果,因为这就是 -O0 中发生的情况,并且所有结果实际上都是由 CPU 在运行时计算的,而不是编译器试图变得聪明。

底线,我的问题是,这是根据我不知道的某些规则的“预期行为”,还是我在三个不同的编译器(GCC、Clang 和 ICX)中发现了错误? (我无法测试 MSVC,因为 Goldbolt 不支持执行这些构建的代码。)

(-fno-strict-aliasing 不会影响结果,因此 ((int32_t*)(char*)mask_field)[i] 不会导致这个。)

最佳答案

NaN 结果的符号仅针对 abs、copysign 和一元减号指定。否则,该符号未指定。当 SSE 指令的两个操作数都不是 NaN 时,x86 CPU 会产生负 NaN,但编译器在优化时没有义务模拟它。

因此,只有 mask_bits 变量的低两位是可预测的。

例如,对于 gcc -O1,编译器会在内部将 _mm256_sub_ps(a, a) 转换为 a + b,其中b 是一个常量 vector ,其内容与 a 相同,但所有符号都翻转了。之后,它发出 vaddps 指令,其中包含寄存器上的常量 vector ,结果的高半字节中的位取决于操作数的顺序(CPU 从操作数之一复制 NaN)。

LLVM 将无穷大减法折叠为正 NaN,其中 CPU 生成负 NaN:https://godbolt.org/z/1hd69josr

关于C++ 编译器为 AVX SIMD 代码中从自身减去 +-Infinity 或 +-NaN 的恒定传播给出不同的 NaN 符号,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76194413/

相关文章:

C++ RTSP视频采集实现

c - Visual Studio 中是否有等效的语句表达式?

gcc - mulx 指令的固有特性

linux - 在 nasm 程序集中的 .data 段之外声明字符串

xcode - xcrun clang --sysroot 找不到 stdio.h

c++ - 为什么 clang 和 gcc 以不同的方式处理具有类内初始化的结构的支撑初始化?

c++ - C++ 中的模板类,列表示例

c++ - 从内存中加载动态库

c++ - Boost 序列化加载失败并抛出异常

c++ - 如何防止在 std header (在 Xcode 中)内发生编译错误?