【问题标题】:The advantages of using 32bit registers/instructions in x86-64在 x86-64 中使用 32 位寄存器/指令的优点
【发布时间】:2016-11-13 04:30:06
【问题描述】:

有时 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 位寄存器/操作而不是 64 位寄存器/操作?

【问题讨论】:

  • 如果你objdump -d 创建了目标文件,你会看到xorq 需要一个额外的编码字节。有关详细信息,请参阅 x86 程序员手册。
  • 这只是一个优化。代码大小(可以说也是性能,管道中的内容越多,缓存中的内容越多)。 x86 从 16 位开始,然后是 32 位扩展,然后是 64 位。根据您的工具,其中一些指令可能在 32 位或 64 位上使用相同的操作码。有时它只是反汇编程序误导您,有时它实际上是一个较小的寄存器和零扩展或符号扩展或其他。只需阅读 x86 文档。

标签: gcc assembly x86-64 micro-optimization


【解决方案1】:

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

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

为什么 gcc 停在xorl 而不使用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 位操作数大小使用单独的操作码,而不是前缀,但仍然可能有部分寄存器的惩罚。

另请参阅Why doesn't GCC use partial registers? 32 位寄存器被视为部分寄存器,因为写入它们总是会写入整个 64 位寄存器。 (主要问题是写入部分 reg,而不是在全角写入后读取它们。)

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

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

在所有 CPU 上,代码大小对于解码和 I-cache 占用量总是很重要(除非前面的代码更小,后面的 .p2align 指令只会增加填充量1 )。使用 32 位操作数大小进行异或归零(或一般隐式零扩展而不是显式2,包括使用AVX vpxor xmm0,xmm0,xmm0 to zero AVX512 zmm0。)没有缺点。)

对于所有操作数大小,大多数指令的速度都相同,因为现代 x86 CPU 可以为宽 ALU 提供晶体管预算。例外情况包括 imul r64,r64 is slower than imul r32,r32 on AMD CPUs before Ryzen, and Intel Atom,并且 64 位 div 在所有 CPU 上都明显变慢。 AMD pre-Ryzen 速度较慢popcnt r64。 Atom/Silvermont 的速度较慢 shld/shrd r64r32。主流 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 位 but it can still be more efficient to use a 32-bit add instead of 16-bit 的情况。

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


脚注 1:很少有使指令超出必要时间的用例 (What methods can be used to efficiently extend instruction length on modern x86?)

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

  • 调整特定微架构的前端(即通过控制指令边界的位置来优化解码)。插入 NOP 会花费额外的前端带宽,完全无法达到目的。

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

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

【讨论】:

  • 为了清楚起见:REX 前缀是指令的前缀而不是寄存器的前缀?
  • @ead:是的。有关 insn 编码的详细信息,请参阅 Intel insn ref 手册。
  • 我花了一些时间对各种 x86 架构上使用 16 位操作数和 32 位操作数的算术密集型代码进行基准测试,并且操作数大小前缀产生了 令人惊讶 的数量的区别。 16 位特化比简单地将 16 位值符号扩展为 32 位、使用 32 位指令并截断结果要慢 50-100%。从 Pentium III 一直到 Sandy Bridge 都是如此。我很惊讶,我有点想知道为什么编译器仍然费心发出 16 位指令。我还没有发现它们更快的情况。
  • 我对您在这里的说法很感兴趣,即“使用更大的指令而不是使用 NOP 填充通常更有效”。我以前从未在任何地方听到过这种智慧。这是您通过测试发现的,还是在某处记录的?以及为什么这可能是真的的任何想法?与更常用的指令相比,仅仅是解码器没有针对各种 NOP 编码进行优化吗?
  • @CodyGray:Agner Fog 在他的优化装配指南中建议使用更长的编码进行对齐。它仅适用于将执行 NOP 的情况,例如对齐您进入的循环的顶部,而不是跳入中间循环的入口点。 NOP 仍然在解码器中占据一席之地,并在 uop 缓存和发布阶段作为融合域 uop。它们不占用执行单元,但这通常不是具有大量执行单元的现代 CPU 的瓶颈。
【解决方案2】:

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

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

【讨论】:

    猜你喜欢
    • 2012-06-25
    • 2011-10-26
    • 2012-02-26
    • 2019-12-04
    • 2017-05-30
    相关资源
    最近更新 更多