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-uopcvtss2sd
仅在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
将是最紧凑的(尽管这确实会延长关键路径延迟,因为移动消除在这方面不起作用。)