【问题标题】:How does RIP-relative addressing perform compared to mov reg, imm64?与 mov reg, imm64 相比,RIP 相对寻址的性能如何?
【发布时间】:2018-01-16 19:39:09
【问题描述】:

众所周知,x86-64 指令不支持 64 位立即数(mov 除外)。因此,当将代码从 32 位迁移到 64 位时,如下所示的指令:

    cmp rax, addr32

不能替换为以下内容:

    cmp rax, addr64

在这种情况下,我正在考虑两种选择:(a) 使用暂存寄存器加载常量或 (b) 使用 rip-relative 寻址。这两种方法如下所示:

    mov r11, addr64 ; scratch register
    cmp rax, r11

ptr64: dq addr64

...
     cmp rax, [rel ptr64]    ; encoded as cmp rax, [rip+offset]

我写了一个非常简单的循环来比较两种方法的性能(我粘贴在下面)。虽然(b)使用间接指针,但(a)在指令中编码了立即数(这可能导致 i-cache 的使用更差)。令人惊讶的是,我发现 (b) 的运行速度比 (a) 快约 10%。这个结果在更常见的现实世界代码中是可以预期的吗?


true:  dq 0xFFFF0000FFFF0000
false: dq 0xAAAABBBBAAAABBBB

main:
    or rax, 1  ; rax is odd and constant "true" is even
    mov rcx, 0x1
    shl rcx, 30
branch:
    mov r11, 0xFFFF0000FFFF0000 ; not present in (b)
    cmp rax, r11                ; vs cmp rax, [rel true]
    je next
    add rax, 2
    loop branch

next:
    mov rax, 0
    ret

【问题讨论】:

  • mov rcx, 0x1 / shl rcx, 30mov ecx, 1<<30 具有 优势。另外,为什么你希望rax 的起始值依赖于 CRT 启动代码留在rax 中的任何垃圾?
  • 是的,我知道,我只是懒惰,不想写一个非常大的常数,然后计数为零:)。至于rax,我对它的价值不感兴趣,但对永远不会被占用的je next 感兴趣。在我看来,具有不可预测的初始值在某种程度上听起来是一个很好的属性,但我想这并不重要,它只是令人困惑。
  • 这就是为什么你让汇编器为你生成常量,但字面意思是mov ecx, 1<<30。这是有效的 NASM 语法:nasm.us/doc/nasmdoc3.html#section-3.5.4。此外,您可以通过使用cmp / jnz 作为循环分支来简化这一点。 (并且只需选择您的 64 位常量,以便循环在合理的时间内运行。)循环内的额外未采用分支会对某些 CPU 上的微小循环产生吞吐量影响。 stackoverflow.com/questions/47783926/….

标签: performance assembly x86-64


【解决方案1】:

令人惊讶的是,我发现 (b) 的运行速度比 (a) 快约 10%

您可能在 AMD Bulldozer 系列或 Ryzen 以外的 CPU 上进行了测试,这些 CPU 具有快速的 loop 指令。在其他 CPU 上,loop is very slow, mostly on purpose for historical reasons,因此您会遇到瓶颈。例如7 个微指令,在 Haswell 上每 5c 吞吐量一个。

mov r64, imm64 不利于 uop 缓存吞吐量,因为 Intel 的 uop 缓存中立即占用 2 个插槽。 (请参阅 Agner Fog's microarch pdf 中的 Sandybridge uop 缓存部分)和我列出了详细信息的 Which is faster, imm64 or m64 for x86-64?

除此之外,循环中的 1 个额外 uop 使其运行速度变慢也就不足为奇了。您可能不在 AMD CPU 上(单微指令 / 每 2 个时钟 1 个 loop),因为在如此小的循环中额外的 mov 会产生超过 10% 的差异。或者根本没有区别,因为它只是每 2 个时钟 3 对 4 uop,如果这是正确的话,即使是很小的 loop 循环也被限制为每 2 个时钟一次跳转。

在 Intel 上,loop 是 7 uop,在大多数 CPU 上每 5 个时钟吞吐量 1 个,因此每时钟 4 个问题/重命名瓶颈不会是您遇到的问题。 loop 是微编码的,所以前端不能从循环缓冲区中运行。 (并且 Skylake CPU 通过微码更新禁用了 LSD,以修复部分寄存器错误。)因此,每次循环都必须从 uop 缓存中重新读取 mov r64,imm64 uop。


在缓存中命中的负载具有非常好的吞吐量(每个时钟 2 个负载,在这种情况下,微融合意味着没有额外的微指令来使用内存操作数而不是寄存器 @ 987654336@)。因此,使用内存中的常量的主要惩罚是额外的缓存占用和缓存未命中,但您的微基准测试根本不会显示这一点。它对负载端口也没有其他压力。


一般情况下:

如果可能,请使用相对于 RIP 的 lea 来生成 64 位地址常量。
例如lea rax, [rel addr64]
是的,这需要一条额外的指令才能将常量放入寄存器。 (顺便说一句,只需使用default rel。如果需要,可以使用[abs fs:0]

如果您使用默认(小)代码模型构建位置相关代码,则可以避免额外的指令,因此静态地址适合虚拟地址空间的低 32 位,并且可以用作立即数。 (实际上低 2GiB,所以符号或零扩展都有效)。如果 gcc 抱怨绝对寻址,请参阅32-bit absolute addresses no longer allowed in x86-64 Linux?-pie 在大多数发行版上默认启用。这当然不适用于 Linux 共享库,后者仅支持 64 位地址的文本重定位。但是您应该尽可能避免重定位,方法是使用lea 制作与位置无关的代码。

大多数整数构建时常量适合 32 位,因此即使在 PIC 代码中也可以使用 cmp r64, imm32cmp r32, imm32

如果您确实需要 64 位非地址常量,请尝试将 mov r64, imm64 提升到循环之外。如果mov 不在循环内,您的cmp 循环会很好。 x86-64 有足够的寄存器,您(或编译器)通常可以避免在整数代码的最内层循环内重新加载。

猜你喜欢
  • 2021-06-11
  • 1970-01-01
  • 2015-09-22
  • 2018-06-15
  • 2011-03-16
  • 2012-09-02
  • 2013-04-07
相关资源
最近更新 更多