为什么这个不必要的 MOVAPD 复制在 gcc 9.1 中,在一个小函数中

Why this unnecessary MOVAPD copy in gcc 9.1, in a tiny function

考虑以下代码:

double x(double a,double b) {
    return a*(float)b;
}

它执行从 doublefloat 的转换,然后再次转换为 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

使用 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 指令有什么好处吗?

这个问题与 Assembly code redundancy in optimized C code 类似,但我认为它不一样,因为 gcc 的旧版本不会生成不必要的副本。

这是 GCC 遗漏的优化;不幸的是,对于 GCC 在微型函数 中这种情况并不少见,因为它的寄存器分配器在调用约定强加的硬寄存器约束下表现不佳;显然 GCC 在较大函数的部分之间通常不会像这样愚蠢。

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

但不幸的是,即使对于 SSE2(Pentium 4 的新功能),他们也保留了这种模式。并且后来拒绝用 SSE 指令的 AVX 版本修复它。因此,编译器只能在通过错误依赖创建长循环携带依赖链或使用 pxor-zeroing 的风险之间做出选择。 GCC 保守地总是在 -O3 处使用 pxor,在 -Os 处省略它。 (像 mulsd 这样的 2 源操作已经依赖于作为输入的目的地,所以这是不必要的)。

在这种情况下,由于其寄存器分配选择不当,遗漏 pxor-归零将意味着将 (float)b 转换回 double 直到 [=19] 才能开始=] 准备好了。因此,如果关键路径是 a 准备就绪(b 提前准备好),则忽略它会增加 a-> Skylake 上结果的延迟 5 个周期(对于 2-uop cvtss2sd 到 运行 只有在 a 准备好之后,因为输出必须合并到最初保存 a 的寄存器中。)否则只有 mulsd等待 a,所有涉及 b 的事情都提前完成。

foo same,same 是解决输出依赖的另一种方法;这就是 clang 正在做的事情。 (以及 GCC 试图为 popcnt 做的事情,它出人意料地在 Sandybridge 系列上有一个架构上不需要的,不像这些愚蠢的 SSE 那样。)

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

相关:(我应该把它链接起来,我忘了我最近已经在 Stack Overflow 上写了这么多细节。)


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