我正在研究如何检测 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
:
-O0
:0xac - 新的 NaN 是 -nan
;保留输入 NaN 的符号。-O1
:0x5c - 新的 NaN 是 -nan
,输入 NaN 的翻转符号。-O0
:0xac-O1
:0xa0 - 新的 NaN 是 +nan
;保留输入 NaN 的符号。-O0
:0xac-O1
:0xa0PRINT_ALL
,优化的 GCC 现在与硬件的功能相匹配,LLVM(clang 和 ICX)没有改变。
-O0
:0xac-O1
:0xac-O0
:0xac-O1
:0xa0-O0
:0xac-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]
不是造成这个的原因。)
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