C++ 编译器给出不同的 NaN 符号,用于在 AVX SIMD 代码中从自身减去 +-Infinity 或 +-NaN 的不断传播

问题描述 投票:0回答:1

我正在研究如何检测 SIMD 寄存器的哪些通道中的浮点数是 +/- 无穷大或 +/- 南。在运行时看到一些奇怪的行为后,我决定将东西扔到 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)没有改变。
    • 海湾合作委员会 13.1
      -O0
      :0xac
    • 海湾合作委员会 13.1
      -O1
      :0xac
    • Clang 16.0.0
      -O0
      :0xac
    • 铛 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?C
0x?0
。该半字节的低 2 位来自
0-0
1-1
元素,它们产生
+0.0
输出 as required for finite
same-same
with rounding mode other than towards -Inf (这不是默认值。 )

ICX 和 clang 似乎彼此一致,但根据优化级别的不同,结果仍然不同。我猜 0xac 是正确的结果,因为这就是

-O0
中发生的事情,所有结果实际上都是由 CPU 在运行时计算的,编译器没有试图变得聪明。

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

-fno-strict-aliasing
不影响结果,所以
((int32_t*)(char*)mask_field)[i]
不是造成这个的原因。)

c++ gcc clang nan avx
1个回答
1
投票

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

因此只有

mask_bits
变量的低两位是可预测的。

例如,对于

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

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

© www.soinside.com 2019 - 2024. All rights reserved.