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



有时 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

对于未签名的分区,需要0寄存器%rdx。这可以通过xorq %rdx, %rdx来实现,但xorl %edx, %edx似乎具有相同的效果。

至少在我的机器上,xorl没有性能提升(即加速)超过 xorq.

我实际上不止一个问题:

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

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

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

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

写入 8 位或 16 位部分寄存器不会将零扩展到寄存器的其余部分。 (仅写入 32 位寄存器隐式零扩展为 64)

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

参见 为什么 GCC 不使用部分寄存器? 32 位寄存器不被视为部分寄存器,因为写入寄存器总是写入整个 64 位寄存器。 (它的主要问题是写入部分注册表,而不是在全角写入后读取它们。

是否有机器的 xorl 比 xorq 快?

是的,Silvermont/KNL 仅将xor -归零识别为具有 32 位操作数大小的归零习语(依赖关系中断和其他好东西)。 因此,即使代码大小相同,xor %r10d, %r10d也比xor %r10, %r10好得多。 (xor需要 REX 前缀用于r10,无论操作数大小如何)。

在所有 CPU 上,代码大小始终对解码和 I 缓存占用空间有潜在影响(除非前面的代码小于1,则后面的 .p2align 指令只会进行更多填充)。 使用 32 位操作数大小进行异或归零(或一般隐式零扩展而不是显式2,包括使用 AVX vpxor xmm0,xmm0,xmm0到零 AVX512 zmm0)没有缺点。

对于所有操作数大小,大多数指令的速度都相同,因为现代 x86 CPU 可以承受宽 ALU 的晶体管预算。 例外情况包括imul r64,r64在Ryzen和Intel Atom之前的AMD CPU上比imul r32,r32慢,并且在所有CPU上div 64位明显慢。 AMD在锐龙之前的popcnt r64速度较慢。 Atom/Silvermont 的shld/shrd r64速度很慢 vs. r32 . 主流英特尔(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 位加法仍然更有效。

另请参阅 http://agner.org/optimize/和 x86 标记 wiki。


脚注 1:很少有用例使指令超过必要的长度(在现代 x86 上可以使用哪些方法来有效地扩展指令长度?

  • 在不需要 NOP 的情况下对齐后面的分支目标。

  • 针对特定微架构的前端进行调优(即通过控制指令边界的位置来优化解码)。 插入NOP将花费额外的前端带宽,并完全违背整个目的。

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

脚注 2:我发现了隐式零扩展至少与更广泛的操作一样便宜的规则的一个例外:由 256 位指令读取的 Haswell/Skylake AVX 128 位负载具有额外的 1c 存储转发延迟,而不是由 128 位指令消耗。 (详情见Agner Fog博客论坛上的一个帖子。

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

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

最新更新