为什么 gcc 在只使用 SS/SD 指令的较低值时不将 XMM 寄存器的较高值归零?

Why doesn't gcc zero the upper values of an XMM register when only using the lower value with SS/SD instructions?

例如这样的功能,

int fb(char a, char b, char c, char d) {
    return (a + b) - (c + d);
}

gcc 的汇编输出是,

fb:
        movsx   esi, sil
        movsx   edi, dil
        movsx   ecx, cl
        movsx   edx, dl
        add     edi, esi
        add     edx, ecx
        mov     eax, edi
        sub     eax, edx
        ret

依稀明白movsx的目的是去除寄存器先前值的依赖,但老实说我还是不明白它到底想去除什么样的依赖。我的意思是,例如,无论是否存在 movsx esi, sil,如果正在向 esi 写入某些值,那么如果正在读取值,则使用 esi 的任何操作都必须等待从 esi 开始,任何修改 esi 值的操作都必须等待,如果 esi 没有被任何操作使用,代码将继续到 运行。 movsx 有什么区别?我不能说编译器做错了,因为 movsxmovzx(几乎?)总是在加载小于 32 位的值时由任何编译器生成。

除了我缺乏理解外,gccfloat 的行为不同。

float ff(float a, float b, float c, float d) {
    return (a + b) - (c + d);
}

编译为,

ff:
        addss   xmm0, xmm1
        addss   xmm2, xmm3
        subss   xmm0, xmm2
        ret

如果应用相同的逻辑,我相信输出应该是这样的,

ff:
        movd    xmm0, xmm0
        movd    xmm1, xmm1
        movd    xmm2, xmm2
        movd    xmm3, xmm3
        addss   xmm0, xmm1
        addss   xmm2, xmm3
        subss   xmm0, xmm2
        ret

所以我实际上问了 2 个问题。

  1. 为什么 gccfloat 的行为不同?
  2. movsx有什么区别?
  1. return 值与 args 的宽度相同,因此不需要扩展名。在 x86 和 x86-64 调用约定中,类型宽度之外的寄存器部分允许存放垃圾。 (这适用于 GP 整数和向量寄存器。)

    除了 clang 依赖的未记录的扩展,调用者将窄参数扩展到 32 位; clang 将跳过 char 示例中的 movsx 指令。 https://godbolt.org/z/Gv5e4h3Eh

    涵盖高垃圾和调用约定的非官方扩展。

    既然你询问了错误的依赖关系,请注意编译器确实使用movaps xmm,xmm来复制标量。 (例如,在 GCC 在 (a-b) + (a-d) 中错过的优化中,我们需要从 a 中减去两次。它是不可交换的,所以我们需要一个副本:https://godbolt.org/z/Tvx19raa3
    实际上,movss xmm1, xmm0 依赖于 XMM1,而 movaps 没有,如果您实际上并不关心与旧的高字节。

    (Pentium III 或 Pentium M 的调整使用 movss 可能有意义,因为它在那里是单 uop,但当前的 GCC -O3 -m32 -mtune=pentium3 -mfpmath=sse 使用 movaps,花费第二个 uop 以避免错误依赖。直到 Core2,P6 系列的 SIMD 执行单元才扩大到 128 位,与 Pentium 4 匹配。)

  2. C 整数提升规则 意味着 a+b 对于窄输入等价于 (int)a + (int)b。在所有 x86 / x86-64 ABI 中,char 是有符号类型(例如与 ARM 不同),因此需要 sign 扩展到 int 宽度,不是零扩展。而且绝对不会被截断。

    如果您通过 returning a char 再次截断结果,编译器可以只执行 8 位加法。但实际上他们将使用 32 位添加并在那里留下任何高垃圾:https://godbolt.org/z/hGdbecPqv。这样做不是为了破坏 dep/性能,只是为了正确性。

    就性能而言,如果调用者写了完整的寄存器(调用约定的非官方扩展无论如何都需要),或者在不与 reg 的其余部分分开重命名低 8 的 CPU(除 P6 系列之外的所有内容:SnB 系列仅重命名高 8 reg,原始 Sandybridge 本身除外。


PS:没有像 movd xmm0, xmm0 这样的指令,只有 movq xmm0, xmm0 的另一种形式,它会将 XMM 寄存器的低 64 位零扩展到完整的 reg .

如果您想查看各种编译器尝试对低位双字进行零扩展,with/without SSE4.1 insertps,请查看 Godbolt [=77] 中 __m128 foo(float f) { return _mm_set_ss(f); } 的 asm =] 以上。例如仅使用 SSE2,使用 pxor 将寄存器归零,然后 movss xmm1, xmm0。否则,insertps 或 xor-zero 和 blendps.