在 x86-64 中使用 32bit registers/instructions 的优点

The advantages of using 32bit registers/instructions in x86-64

有时 gcc 使用 32 位寄存器,而我希望它使用 64 位寄存器。例如下面的 C 代码:

unsigned long long 
div(unsigned long long a, unsigned long long b){
    return a/b;
}

使用 -O2 选项编译为(省略一些样板内容):

div:
    movq    %rdi, %rax
    xorl    %edx, %edx
    divq    %rsi
    ret

对于无符号除法,寄存器%rdx需要为0。这个可以通过xorq %rdx, %rdx来实现,但是xorl %edx, %edx好像也有同样的效果。

至少在我的机器上 xorlxorq 没有性能提升(即加速)。

其实我的问题不止一个:

  1. 为什么 gcc 更喜欢 32 位版本?
  2. 为什么 gcc 停在 xorl 而没有使用 xorw
  3. 有机器 xorlxorq 快吗?
  4. 如果可能,是否应该总是更喜欢 32 位 register/operations 而不是 64 位 register/operations?

在 64 位模式下,写入 32 位寄存器会将高 32 位清零 => xorl %edx, %edxrdx 的上部归零为 "free"。

另一方面,xor %rdx, %rdx 是用一个额外的字节编码的,因为它需要一个 REX 前缀。 尝试将 64 位寄存器置零时,将其作为 32 位寄存器进行异或显然是一个胜利。

Why does gcc prefer the 32bit version?

主要是代码大小:机器代码编码中不需要 REX 前缀。

Why does gcc stop at xorl and doesn't use xorw?

写入 8 位或 16 位部分寄存器不会零扩展到寄存器的其余部分。 (Only writing a 32-bit register implicitly zero-extends to 64)

此外,xorw 需要操作数大小前缀进行编码,因此它的大小与 xorq 相同,大于 xorl32 位操作数大小是 x86-64 机器代码中的默认值,不需要前缀。(对于大多数指令;一些像 push/popcall/jmp 默认为 64 位,包括内存间接 call [rdi] = ff 17 和内存中的指针。)8 位操作数大小使用单独的操作码,而不是前缀, 但仍然可能有部分注册惩罚。

另见 32 位寄存器 不是 考虑的部分寄存器,因为写入它们总是写入整个 64 位寄存器。 (主要问题是写入部分寄存器,而不是在全角写入后读取它们。)

Are there machines for which xorl is faster than xorq?

是的,Silvermont / KNL 只识别具有 32 位操作数大小的 (依赖关系破坏和其他好东西)。因此,即使代码大小相同,xor %r10d, %r10d 也比 xor %r10, %r10 好得多。 (xor 需要 r10 的 REX 前缀,无论操作数大小如何)。

在所有 CPU 上,代码大小总是对解码和 I-cache 占用空间有潜在影响(除非后面的 .p2align 指令如果前面的指令只会产生更多填充代码更小1)。使用 32 位操作数大小进行异或归零(或一般隐式零扩展而不是显式 2,包括使用 .)[=54 没有任何缺点=]

大多数指令对于所有操作数大小都是相同的速度,因为现代 x86 CPU 可以负担得起宽 ALU 的晶体管预算。例外情况包括 ,并且 64 位 div 在所有 CPU 上都明显较慢。 AMD pre-Ryzen 速度较慢 popcnt r64。 Atom/Silvermont 比 r32shld/shrd r64。主流 Intel(Skylake 等)速度较慢 bswap r64


Should one always prefer 32bit register/operations if possible rather than 64bit register/operations?

是的,至少出于代码大小的原因,更喜欢 32 位操作,但请注意,在指令中的任何位置(包括寻址模式)使用 r8..r15 也会需要一个 REX 前缀。因此,如果您有一些数据,您可以使用 32 位操作数大小(或指向 8/16/32 位数据的指针),更愿意将其保存在低 8 位命名寄存器中(e/rax..)比高 8 编号的寄存器。

但是不要花费额外的指令来实现它;节省几个字节的代码大小通常是最不重要的考虑因素。 例如只需使用 r8d 而不是 saving/restoring rbx 因此如果您需要一个不必保留调用的额外寄存器,则可以使用 ebx。使用 32 位 r8d 而不是 64 位 r8 对代码大小没有帮助,但对于某些 CPU 上的某些操作来说它可以更快(见上文)。

这也适用于您只关心寄存器的低 16 位的情况,

另请参阅 http://agner.org/optimize/ and the 标签 wiki。


脚注 1:很少有使指令长于必要的用例 ()

  • 无需 NOP 对齐后面的分支目标。

  • 针对特定微体系结构的前端进行调优(即通过控制指令边界的位置来优化解码)。插入 NOP 会消耗额外的前端带宽并完全破坏整个目的。

汇编程序不会为您做这些,并且每次更改任何内容时都需要重新进行手动操作非常耗时(并且您可能必须使用 .byte 指令来手动编码指令) .

脚注 2:我发现隐式零扩展至少与更广泛的操作一样便宜的规则有一个例外:Haswell/Skylake AVX 128-与 128 位指令相比,256 位指令读取的位加载具有额外的 1c 存储转发延迟。 (详情in a thread on Agner Fog's blog forum。)