在 x86-64 中使用 32 位寄存器/指令的优点

2024-04-02

有时 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, but xorl %edx, %edx似乎有同样的效果。

至少在我的机器上没有性能增益(即加速)xorl over xorq.

我其实有不止一个问题:

  1. 为什么gcc更喜欢32位版本?
  2. 为什么 gcc 停止在xorl并且不使用xorw?
  3. 是否有机器xorlxorq?
  4. 如果可能的话,人们是否应该总是选择 32 位寄存器/操作而不是 64 位寄存器/操作?

为什么gcc更喜欢32位版本?

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

为什么 gcc 停止在xorl并且不使用xorw?

写入 8 位或 16 位部分寄存器不会零扩展到寄存器的其余部分。 (仅写入 32 位寄存器隐式零扩展到 64 https://stackoverflow.com/questions/11177137/why-do-x86-64-instructions-on-32-bit-registers-zero-the-upper-part-of-the-full-6)

除了,xorw需要一个操作数大小前缀来编码,所以它的大小与xorq, 比大xorl. 32 位操作数大小是 x86-64 机器代码中的默认值,不需要前缀。(对于大多数指令;一些指令如push/pop and call/jmp默认为 64 位,包括内存间接call [rdi] = ff 17与内存中的指针。)8 位操作数大小使用单独的操作码,而不是前缀,但仍然可能存在部分寄存器损失。

也可以看看为什么 GCC 不使用部分寄存器? https://stackoverflow.com/questions/41573502/why-doesnt-gcc-use-partial-registers32位寄存器是not被视为部分寄存器,因为写入它们总是写入整个 64 位寄存器。 (主要问题是写入部分寄存器,而不是在全角写入后读取它们。)

是否存在 xorl 比 xorq 更快的机器?

是的,Silvermont / KNL 只承认xor-zeroing 作为归零惯用语 https://stackoverflow.com/questions/33666617/what-is-the-best-way-to-set-a-register-to-zero-in-x86-assembly-xor-mov-or-and(依赖关系破坏,以及其他好东西)具有 32 位操作数大小。因此,即使代码大小相同,xor %r10d, %r10dxor %r10, %r10. (xor需要 REX 前缀r10无论操作数大小)。

On all CPUs, code size always potentially matters for decode and I-cache footprint (except when a later .p2align directive would just make more padding if the preceding code is smaller1). There's no downside to using 32-bit operand size for xor-zeroing (or to implicit zero-extending in general instead of explict2, including using AVX vpxor xmm0,xmm0,xmm0 to zero AVX512 zmm0 https://stackoverflow.com/questions/43713273/is-vxorps-zeroing-on-amd-jaguar-bulldozer-zen-faster-with-xmm-registers-than-ymm.)

大多数指令对于所有操作数大小都具有相同的速度,因为现代 x86 CPU 可以承担宽 ALU 的晶体管预算。例外情况包括imul r64,r64慢于imul r32,r32在 Ryzen 之前的 AMD CPU 和 Intel Atom 上 https://stackoverflow.com/questions/37925143/x86-64-is-imul-faster-than-2x-shl-2x-add/37925245#37925245和 64 位div所有 CPU 上的速度都明显变慢。 AMD Ryzen 之前的版本速度较慢popcnt r64。 Atom/Silvermont 速度慢shld/shrd r64 vs. r32。主流Intel(Skylake等)速度较慢bswap r64.


如果可能的话,人们是否应该总是选择 32 位寄存器/操作而不是 64 位寄存器/操作?

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

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

这也适用于您只关心寄存器的低 16 位的情况,但使用 32 位加法而不是 16 位仍然会更有效 https://stackoverflow.com/questions/34377711/which-2s-complement-integer-operations-can-be-used-without-zeroing-high-bits-in.

也可以看看http://agner.org/optimize/ http://agner.org/optimize/x86 /questions/tagged/x86标签维基。


脚注1:极少数情况下指令的长度会超过必要的长度(可以使用哪些方法来有效地扩展现代 x86 上的指令长度? https://stackoverflow.com/questions/48046814/what-methods-can-be-used-to-efficiently-extend-instruction-length-on-modern-x86)

  • 无需 NOP 即可对齐稍后的分支目标。

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

汇编程序不会为您执行此操作,并且每次更改任何内容时都要手动执行此操作非常耗时(并且您可能必须使用.byte手动编码指令的指令)。

脚注2:我发现隐式零扩展至少与更广泛的操作一样便宜这一规则的一个例外:由 256 位指令读取的 Haswell/Skylake AVX 128 位负载有额外的 1c 存储转发延迟与被 128 位指令消耗相比。 (细节在 Agner Fog 的博客论坛上的一个帖子中 http://www.agner.org/optimize/blog/read.php?i=415#854.)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在 x86-64 中使用 32 位寄存器/指令的优点 的相关文章

随机推荐