_mm_max_ss 在 clang 和 gcc 之间有不同的行为

问题描述

我正在尝试使用 clang 和 gcc 交叉编译一个项目,但是在使用 _mm_max_ss 时我看到了一些奇怪的差异,例如

__m128 a = _mm_set_ss(std::numeric_limits<float>::quiet_NaN());
__m128 b = _mm_set_ss(2.0f);
__m128 c = _mm_max_ss(a,b);
__m128 d = _mm_max_ss(b,a);

现在,当涉及到 NaN 时,我预计 std::max 类型的行为,但 clang 和 gcc 给出不同的结果:

Clang: (what I expected)
c: 2.000000 0.000000 0.000000 0.000000 
d: nan 0.000000 0.000000 0.000000 

Gcc: (Seems to ignore order)
c: nan 0.000000 0.000000 0.000000 
d: nan 0.000000 0.000000 0.000000 

_mm_max_ps 当我使用它时会做预期的事情。我试过使用 -ffast-math,-fno-fast-math 但它似乎没有效果。有什么想法可以使不同编译器的行为相似吗?

Godbolt 链接 here

解决方法

我的理解是 IEEE-754 要求:(NaN cmp x) 返回 false for all cmp 操作符 {==,<,<=,>,>=},除了 {{1} } 返回 {!=}true 函数的实现可以根据任何不等运算符进行定义。

那么,问题是,max() 是如何实现的?用_mm_max_ps,还是有点比较?

有趣的是,当禁用链接中的优化时,gcc 和 clang 都会使用相应的 {<,>=} 指令。两者产量:

maxss

这表明,给定:2.000000 0.000000 0.000000 0.000000 nan 0.000000 0.000000 0.000000 ,即:max(NaN,2.0f) -> 2.0f,其中 max(a,b) = (a op b) ? a : b 是:op 之一。根据 IEEE-754 规则,此比较的结果始终为假,因此:

{<,>=} 总是 false,返回 (NaN op val),
(val) 总是 false,返回 (val op NaN)

启用优化后,编译器可以在编译时自由地预先计算 (NaN)(c)。似乎 clang 将结果评估为 (d) 指令将 - 更正“as-if”行为。 GCC 要么退回到 maxss 的另一种实现上——它使用 GMP 和 MPFR 库来处理编译时数字——或者只是对 max() 语义粗心大意。

GCC 在 Godbolt 上的 10.2 和主干版本仍然出错。所以我认为你发现了一个错误!我还没有回答第二部分,因为我想不出一个通用的 hack 可以有效地解决这个问题。


来自英特尔的 ISA 参考:

如果被比较的值都是 0.0s(任一符号),则值 在第二个源操作数中返回。如果第二个值 源操作数是一个 SNaN,SNaN 原样返回给 目标(即不返回 SNaN 的 QNaN 版本)。

如果该指令只有一个值是 NaN(SNaN 或 QNaN),则 第二个源操作数,可以是 NaN 或有效的浮点值, 写入结果。如果不是这种行为,则需要 返回来自任一源操作数的 NaN,动作 可以使用一系列指令来模拟 MAXSS,例如, 比较后跟 AND、ANDN 和 OR。