Assembly 为什么在gcc 9.1中,在一个很小的函数中会出现这种不必要的MOVAPD拷贝

Assembly 为什么在gcc 9.1中,在一个很小的函数中会出现这种不必要的MOVAPD拷贝,assembly,gcc,x86-64,sse,micro-optimization,Assembly,Gcc,X86 64,Sse,Micro Optimization,考虑以下代码: double x(double a,double b) { return a*(float)b; } 它将double转换为float,然后再转换为double,并相乘 当我在x86/64上用gcc 9.1和-O3编译它时,我得到: x(double, double): movapd xmm2, xmm0 pxor xmm0, xmm0 cvtsd2ss xmm1, xmm1 cvts

考虑以下代码:

double x(double a,double b) {
    return a*(float)b;
}
它将
double
转换为
float
,然后再转换为
double
,并相乘

当我在
x86/64
上用
gcc 9.1
-O3
编译它时,我得到:

x(double, double):
        movapd  xmm2, xmm0
        pxor    xmm0, xmm0
        cvtsd2ss        xmm1, xmm1
        cvtss2sd        xmm0, xmm1
        mulsd   xmm0, xmm2
        ret
x(double, double):
        movapd  xmm2, xmm0
        cvtsd2ss        xmm1, xmm1
        cvtss2sd        xmm0, xmm1
        mulsd   xmm0, xmm2
        ret
使用
clang
和较旧版本的
gcc
我得到了以下信息:

x(double, double):
        cvtsd2ss        xmm1, xmm1
        cvtss2sd        xmm1, xmm1
        mulsd   xmm0, xmm1
        ret
这里我没有将
xmm0
复制到
xmm2
中,这对我来说似乎是不必要的

使用
gcc 9.1
-Os
我得到:

x(double, double):
        movapd  xmm2, xmm0
        pxor    xmm0, xmm0
        cvtsd2ss        xmm1, xmm1
        cvtss2sd        xmm0, xmm1
        mulsd   xmm0, xmm2
        ret
x(double, double):
        movapd  xmm2, xmm0
        cvtsd2ss        xmm1, xmm1
        cvtss2sd        xmm0, xmm1
        mulsd   xmm0, xmm2
        ret
因此,它只是删除将
xmm0
设置为零的指令,而不是
moveapd

我相信这三个版本都是正确的,所以
gcc9.1-O3
版本是否会带来性能上的好处?如果是,为什么?
px或xmm0,xmm0
指令有什么好处吗


这个问题与类似,但我认为不一样,因为较旧版本的
gcc
不会生成不必要的副本。

这是一个gcc遗漏的优化;不幸的是,当GCC的寄存器分配器在调用约定施加的硬寄存器约束下工作不好时,它在小函数中的情况并不罕见;显然,在大型函数的各个部分之间,GCC通常不会像这样愚蠢

pxor
-归零是为了打破
cvtss2sd
的(假)输出依赖关系,这是因为英特尔短视地设计了单源标量指令,不修改目标向量的上半部分。他们从针对PIII的SSE1开始,它提供了一个短期收益,因为PIII将XMM REG处理为两个64位的一半,所以只编写一半就可以让像
sqrtss
这样的指令成为单uop

但不幸的是,他们甚至在SSE2(奔腾4的新版本)中也保留了这种模式。后来拒绝用AVX版本的SSE指令修复。因此,编译器在通过错误的依赖项创建长循环携带的依赖项链,或者使用PX或归零的风险之间选择。GCC保守地总是在
-O3
处使用pxor,在
-Os
处省略它。(像
mulsd
这样的2源操作已经依赖于目标作为输入,因此这是不必要的)

在这种情况下,由于寄存器分配选择不当,忽略
pxor
-归零将意味着在
a
准备就绪之前,无法将
(float)b
转换回
double
。因此,如果关键路径是
a
准备就绪(
b
ready early),忽略它将使Skylake上
a
->结果的延迟增加5个周期(2-uop
cvtss2sd
仅在
a
准备就绪后运行,因为输出必须合并到最初持有
a
的寄存器中)否则,只有
mulsd
必须等待
a
,所有涉及
b
的事情都要提前完成

foo same,same
是解决输出依赖性的另一种方法;那就是叮当在做什么。(GCC试图为popcnt做些什么,它在Sandybridge系列上意外地有一个架构上不需要的,不像这些愚蠢的SSE系列。)

顺便说一句,AVX 3操作数指令有时确实提供了一种解决伪依赖关系的方法,使用“冷”寄存器或异或零寄存器作为要合并的寄存器。包括标量int->FP,尽管clang有时只是使用
movd
加上压缩转换

相关:(我应该把它链接起来,我忘了我最近已经写了很多关于堆栈溢出的细节。)



movapd
pxor
归零在现代CPU上不会花费任何延迟,但没有什么是免费的。它们仍然需要前端uop和代码大小(L1i缓存占用)
movapd
在后端没有延迟,并且不需要执行单元,但仅此而已-

作为一种猜测,我并不特别熟悉这种东西,我要说,
movapd
由于注册重命名,实际上是免费的,这些额外的指令可能会消除一些错误的依赖关系。@ThomasJager:没有什么是免费的。它仍然需要前端uop和代码大小(L1i缓存占用)。它在后端没有延迟,并且不需要执行单元,但仅此而已。(对于pxor归零也一样,GCC只使用它,这要感谢Intel对不归零扩展到目标的单源标量指令的短视错误设计。clang版本中没有错误依赖,它读取和写入同一寄存器,因此
cvtss2sd
输出错误依赖已经在与它相同的寄存器上。)作为对.Clang版本的真正依赖,gcc的版本是最优的,它是哑的,显然是错过了优化。当gcc的寄存器分配器在调用约定施加的硬寄存器约束下做得很差时,这种情况在小函数中会发生得更多;显然,gcc在大函数的各个部分之间通常不会像这样哑actions.@Unlikus:No,在x86/x86-64调用约定中,允许返回值之外的部分寄存器包含垃圾。(与某些RISC调用约定不同,在某些RISC调用约定中,整数寄存器在传递/返回窄值时至少必须进行符号扩展或零扩展)。中的完整详细信息。没有任何用途;
pxor
-归零只需要因为
movapd
missed-optimization。如果您只需要结果的零扩展,
movq xmm0,xmm0
将是最紧凑的(尽管这确实会延长关键路径延迟,因为移动消除在这方面不起作用。)