众所周知,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
答案 0 :(得分:3)
令人惊讶的是,我发现(b)比(a)
快〜10%
您可能在AMD Bulldozer系列或Ryzen以外的CPU上进行了测试,这些CPU具有快速loop
指令。在其他CPU上, loop
is very slow, mostly on purpose for historical reasons,因此您遇到了瓶颈。例如7个uops,Haswell每5c吞吐量一个。
mov r64, imm64
对于uop缓存吞吐量不利,因为在英特尔的uop缓存中立即占用2个插槽。 (请参阅Agner Fog's microarch pdf中的Sandybridge uop缓存部分)和Which is faster, imm64 or m64 for x86-64?,其中列出了详细信息。
即使除此之外,在循环中增加1个uop使其运行速度变慢也不足为奇。您可能不在AMD CPU上(单个uop /每2个时钟1个loop
),因为如此微小的循环中的额外mov
将产生超过10%的差异。或者完全没有区别,因为它每2个时钟只有3个对4个uop,如果这是正确的,甚至很小的loop
循环限制为每2个时钟跳一次。
在英特尔上,loop
为7 uop,大多数CPU每5个时钟吞吐量一个,因此每时钟4个问题/重命名瓶颈不会成为您的目标。 loop
是微编码的,因此前端不能从循环缓冲区运行。 (并且Skylake CPU通过微码更新禁用其LSD以修复部分寄存器错误。)因此每次循环时都必须从uop缓存中重新读取mov r64,imm64
uop。
在缓存中遇到的负载具有非常好的吞吐量(每个时钟有2个负载,而在这种情况下,微融合意味着没有额外的uop来使用内存操作数而不是{ {1}} 强>)。因此,从内存中使用常量的主要原因是额外的缓存占用空间和缓存未命中,但是您的微基准测试根本不会显示出来。它对装载端口没有其他压力。
如果可能,请使用RIP相对cmp
生成64位地址常量
例如lea
。是的,这需要额外的指令才能将常量输入寄存器。 (顺便说一句,只需使用lea rax, [rel addr64]
。如果需要,可以使用default rel
。
如果使用默认(小)代码模型构建位置相关代码,则可以避免额外的指令因此静态地址适合虚拟地址空间的低32位并且可以用作immediates 。 (实际上低2GiB,因此符号或零扩展两种工作)。如果gcc抱怨绝对寻址,请参见32-bit absolute addresses no longer allowed in x86-64 Linux?;大多数发行版默认启用[abs fs:0]
。这当然不适用于Linux共享库,它只支持64位地址的文本重定位。但是,您应该尽可能使用-pie
来制作位置无关的代码来避免重定位。
大多数整数构建时常数适合32位,因此即使在PIC代码中也可以使用lea
或cmp r64, imm32
。
如果确实需要64位非地址常量,请尝试将cmp r32, imm32
提升出一个循环。如果mov r64, imm64
不在循环内,那么cmp
循环就可以了。 x86-64有足够的寄存器,您(或编译器)通常可以避免在整数代码中的最内层循环内重新加载。