这是我的代码:
int f(double x, double y)
{
return std::isnan(x) || std::isnan(y);
}
如果您使用C而不是C ++,只需将
std ::
替换为 __builtin_
(不要简单地删除 std ::
,原因请参见此处:为什么GCC为C ++<cmath>比C<math.h> 更有效地实现isnan()?)。这是汇编代码:
ucomisd %xmm0, %xmm0 ; set parity flag if x is NAN
setp %dl ; copy parity flag to %edx
ucomisd %xmm1, %xmm1 ; set parity flag if y is NAN
setp %al ; copy parity flag to %eax
orl %edx, %eax ; OR one byte of each result into a full-width register
现在让我们尝试一种相同效果的替代配方:
int f(double x, double y)
{
return std::isunordered(x, y);
}
这是该替代方案的汇编代码:
xorl %eax, %eax
ucomisd %xmm1, %xmm0
setp %al
这很棒——我们几乎将生成的代码减少了一半!这是因为
ucomisd
如果其操作数之一是 NAN,则设置奇偶标志位,因此我们可以同时测试两个值,类似于SIMD的方式。你可以在野外看到像原始版本那样的代码,例如:https://svn.r-project.org/R/trunk/src/nmath/qnorm.c 如果我们能让GCC足够聪明,在任何地方都能组合两个
isnan()
调用,那就太酷了。我的问题是:我们能吗?怎么做?我有一些关于编译器工作原理的想法,但不知道在GCC中这种优化可以在哪里执行。基本思路是每当有一对OR'd together的isnan()
(或__builtin_isnan
)调用时,它应该使用同时使用两个操作数的单个ucomisd
指令进行发出。编辑以添加Basile Starynkevitch答案激发的一些研究:
如果我使用-fdump-tree-all进行编译,我会找到两个似乎相关的文件。首先,
*.gimple
包含以下内容(还有更多):D.2229 = x unord x;
D.2230 = y unord y;
D.2231 = D.2229 | D.2230;
在这里,我们可以清楚地看到GCC知道它将(x, x)
传递给isunordered()
。如果我们想要在此级别上通过转换进行优化,规则大致为:“用a unord b
替换a unord a | b unord b
。” 这就是编译我的第二个C代码时得到的结果:
D.2229 = x unord y;
另一个有趣的文件是 *.original
:
return <retval> = (int) (x unord x || y unord y);
这实际上是由-fdump-tree-original
生成的整个非注释文件。对于更好的源代码,它看起来像这样:
return <retval> = x unord y;
显然,同样的转换可以应用于这里(只不过这里使用||
而不是|
)。
但遗憾的是,如果我们修改源代码如下:
if (__builtin_isnan(x))
return true;
if (__builtin_isnan(y))
return true;
return false;
然后我们得到了完全不同的Gimple和原始输出文件,尽管最终汇编代码与以前相同。因此,也许在管道的后期尝试这种转换更好?在带有“if”的版本和原始版本中,“*.optimized”文件(其中之一)显示相同的代码,因此这是一个很好的迹象。
(simplify (or (unordered @0 @0) (unordered @1 @1)) (unordered @0 @1))
这样简单的代码(好吧,最后一个版本可能不是这样,因为有if
)。请提交PR。 - Marc Glisse