在不是地址/指针的值上使用LEA?
我试图了解地址计算指令的工作原理,尤其是使用leaq命令。 然后当我看到使用leaq进行算术计算的例子时,我感到困惑。 例如,下面的c代码,
long m12(long x) {
return x*12;
}
在组装中,
leaq (%rdi, %rdi, 2), %rax
salq $2, $rax
如果我的理解是正确的,leaq应该将任何地址(%rdi,%rdi,2)移到%rax中,这应该是2 *%rdi +%rdi。 我感到困惑的是因为值x存储在%rdi中,这就是存储器地址,为什么%rdi乘以3然后左移这个存储器地址 2等于x乘以12? 当我们将%rdi乘以3时,我们跳转到另一个不保存值x的存储器地址吗?
leaq
不需要在内存地址上操作,并且它计算一个地址,它实际上并不从结果中读取数据,所以直到mov
或类似的东西试图使用它时,它只是一个添加一个数字的深奥方式,加上1,2,4或8倍另一个数字(或在这种情况下相同的数字)。 正如你所看到的,它经常被滥用于数学目的。 2*%rdi+%rdi
只是3 * %rdi
,所以它计算x * 3
而不涉及CPU上的乘法器单元。
同样,对于整数,左移也会使每位移位的值加倍(向右加上零),这要归功于二进制数的工作方式(十进制数的相同方式,右侧乘以10再加上零)。
所以这是滥用leaq
指令来完成乘法3,然后转换结果以实现乘以4的最终结果乘以12而实际上没有实际使用乘法指令(其大概认为会运行得更慢,而且我知道这可能是正确的;猜测编译器通常是一个失败的游戏)。
lea
(见Intel的指令集手动输入)是使用存储器操作数的语法和编码机的移位和相加指令。 这解释了这个名字,但它并不是它唯一的优点。 它从来不会访问内存,所以它就像在C中使用&
请参阅示例如何在x86中仅使用2个连续的leal指令来将寄存器乘以37?
在C中,它就像uintptr_t foo = &arr[idx]
。 请注意&
为您提供arr + idx
的结果,包括缩放arr
的对象大小。 在C中,这会滥用语言语法和类型,但在x86汇编指针和整数是相同的东西。 一切都只是字节,这取决于程序以正确的顺序放置指令以获得有用的结果。
8086的指令集(Stephen Morse)的原创设计师/建筑师可能会或可能没有将指针数学作为主要用例,但现代编译器认为它只是指针/整数算术的另一种选择,而这就是你应该怎么考虑它。
(请注意,16位寻址模式不包括移位,只是[BP|BX] + [SI|DI] + disp8/disp16
,所以LEA对386之前的非指针数学没有用处。请参阅此答案关于32/64位寻址模式的更多信息,尽管该答案使用英特尔语法,如[rax + rdi*4]
而不是此问题中使用的AT&T语法。 )
也许8086架构师确实只是想将地址计算硬件公开为任意用途,因为他们可以在不使用大量额外晶体管的情况下完成这一任务。 解码器必须能够解码寻址模式,并且CPU的其他部分必须能够进行地址计算。 将结果放入寄存器中而不是将其与段寄存器值一起用于存储器访问,不需要额外的晶体管。 Ross Ridge确认原始8086上的LEA重用CPU有效地址解码和计算硬件。
请注意,大多数现代CPU在与常规添加和移位指令相同的ALU上运行LEA 。 它们具有专用的AGU(地址生成单元),但只能用于实际的内存操作数。 有序的Atom是一个例外; LEA比ALU早在管线上运行:输入必须尽快准备好,但输出也会更早准备就绪。 无序执行CPU(绝大多数为现代x86)不希望LEA干扰实际的加载/存储,因此它们在ALU上运行。
lea
具有良好的延迟和吞吐量,但不如吞吐量add
或mov r32, imm32
大多数的CPU,所以只使用lea
时,你可以使用它,而不是保存的指令add
。 (请参阅Agner Fog的x86微型指南和asm优化手册。)
内部实现是无关紧要的,但可以肯定的是,将操作数解码为LEA共享任何其他指令的解码寻址模式的晶体管 。 (所以有硬件重用/共享即使在现代的CPU不执行lea
上AGU)暴露多输入移位和加法指令将采取的操作数的特殊编码的任何其他方式。
因此,当386扩展了寻址模式以包含缩放索引,并且能够在寻址模式下使用任何寄存器,使得LEA更容易用于非指针时,386获得了“免费”的移位和加法ALU指令。
X86-64到了程序计数器(而不是需要读什么便宜访问call
推送)“免费”通过执法机关,因为它增加了RIP-相对寻址方式,使得访问静态数据X86-64位置无关显著便宜代码比在32位PIC。 (RIP相对需要在处理LEA的ALU以及处理实际加载/存储地址的单独AGU中提供特殊支持,但不需要新的指令。)
对于任意算术来说,它和指针一样好,所以把它当作最近的指针是错误的 。 这不是用于非指针的“滥用”或“技巧”,因为在汇编语言中,所有东西都是整数。 它的吞吐量低于add
,但是它几乎可以在任何时候保存一条指令时使用,而且价格便宜。 但它最多可以保存三条指令:
;; Intel syntax.
lea eax, [rdi + rsi*4 - 8] ; 3 cycle latency on Intel SnB-family
; 2-component LEA is only 1c latency
;;; without LEA:
mov eax, esi ; maybe 0 cycle latency, otherwise 1
shl eax, 2 ; 1 cycle latency
add eax, edi ; 1 cycle latency
sub eax, 8 ; 1 cycle latency
在某些AMD CPU上,即使复杂的LEA也只有2个周期的延迟,但是4个指令的序列将会是esi
准备好最终eax
准备就绪的4个周期延迟。 无论哪种方式,这为前端解码和发布节省了3个uops,并且在退出之前一直占用重排序缓冲区中的空间。
lea
具有几个主要优点 ,特别是在32/64位代码中,寻址模式可以使用任何寄存器并可以移位:
lea 1(%rdi), %eax
或lea (%rdx, %rbp), %ecx
。 cmovcc
之前的测试之后可以很方便。 或者可能在具有部分标志停止的CPU上带有加载循环。 x86-64:与位置无关的代码可以使用相对于RIP的LEA来获取指向静态数据的指针。
7字节的lea foo(%rip), %rdi
略大于mov $foo, %edi
(5字节),因此在符号位于低32位的OS上,首选mov r32, imm32
位置相关的代码虚拟地址空间,如Linux。 您可能需要禁用gcc中的默认PIE设置才能使用此设置。
在32位代码中, mov edi, OFFSET symbol
与lea edi, [symbol]
类似,更短且更快。 (在NASM语法中省略OFFSET
。)RIP相对不可用,并且地址适合32位立即数,因此如果需要将静态符号地址存入寄存器mov r32, imm32
没有理由考虑使用lea
而不使用mov r32, imm32
。
除了x86-64模式下的RIP相对LEA,所有这些同样适用于计算指针与计算非指针整数加/移。
有关汇编指南/手册以及性能信息,另请参阅x86标记wiki。
另请参见如果只需要结果的低位部分,则可以使用哪些2的补码整数运算而不将输入中的高位清零? 64位地址大小和32位操作数大小是最紧凑的编码(无额外前缀),所以在可能的情况下lea (%rdx, %rbp), %rcx
使用lea (%rdx, %rbp), %ecx
而不是lea (%rdx, %rbp), %rcx
或lea (%edx, %ebp), %ecx
。
lea (%edx, %ebp), %ecx
总是无用的,但64位地址/操作数大小对于执行64位数学显然是必需的。 (Agner Fog的objconv反汇编器甚至会警告LEA上无用的地址长度前缀,并使用32位操作数大小。)
这个问题与LEA指令的目的是什么相似?但是大多数答案都是以实际指针数据的地址计算来解释它。 这只是一个用途。
LEA用于计算地址。 它不解引用内存地址
在英特尔语法中,它应该更具可读性
m12(long):
lea rax, [rdi+rdi*2]
sal rax, 2
ret
所以第一行等于rax = rdi*3
然后左移是将rax乘以4,这导致rdi*3*4 = rdi*12