请考虑以下代码:
double x(double a,double b) {
return a*(float)b;
}
它将double
转换为float
,然后再次转换为double
并进行乘法运算。
当我使用gcc 9.1
编译,并在x86/64
上使用-O3
时,我得到:
x(double, double):
movapd xmm2, xmm0
pxor xmm0, 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
cvtsd2ss xmm1, xmm1
cvtss2sd xmm0, xmm1
mulsd xmm0, xmm2
ret
因此,它仅删除将xmm0
设置为零的指令,但不删除moveapd
指令。
我认为这三个版本都是正确的,那么gcc 9.1 -O3
版本是否有性能上的好处?如果有,为什么?pxor xmm0,xmm0
指令有什么好处吗?
这个问题类似于优化C代码中的汇编代码冗余,但我认为它并不相同,因为旧版本的gcc
不会生成不必要的副本。
movapd
由于寄存器重命名的缘故实际上是免费的,而那些额外的指令可能会消除一些错误依赖。 - Thomas Jagercvtss2sd
输出的虚假依赖已经与具有输入真实依赖关系的同一寄存器。 Clang的版本是最佳的,gcc的版本很愚蠢,是一个明显的优化遗漏。 当GCC的寄存器分配器受到调用约定强制的硬寄存器约束时,在小型函数中经常发生这种情况。 显然,GCC通常不会像这样在较大的函数部分之间表现愚蠢。 - Peter Cordesxmm0
必须为零吗?但是这样做会使-Os
不正确。 - Unlikuspxor
清零仅因为movapd
错过了优化。 - Peter Cordes