我试图了解地址计算指令的工作原理,尤其是使用leaq
命令。当我看到使用leaq
进行算术运算的例子时,我感到困惑。例如,以下C代码,
long m12(long x) {
return x*12;
}
在汇编中,
leaq (%rdi, %rdi, 2), %rax
salq $2, $rax
如果我的理解是正确的,那么leaq应移动任何地址(%rdi, %rdi, 2)
,该地址应为2*%rdi+%rdi
,评估为%rax
。令我感到困惑的是因为值x存储在%rdi
中,这只是内存地址,为什么%rdi乘以3然后左移这个内存地址乘以2等于x次12?是不是当我们将%rdi
乘以3时,我们跳转到另一个不具有值x的内存地址?
答案 0 :(得分:10)
lea
(see Intel's instruction-set manual entry)是一个使用内存操作数语法和机器编码的移位和加法指令。这解释了名称,但它并不是它唯一有用的东西。它实际上从未访问过内存,所以它就像在C中使用&
一样。 / p>
参见例如How to multiply a register by 37 using only 2 consecutive leal instructions in x86?
在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位寻址模式的更多信息,请参阅this answer,尽管该答案使用英特尔语法,如[rax + rdi*4]
,而不是此问题中使用的AT& T语法.x86机器代码是相同的,无论什么您用来创建它的语法。)
也许8086架构师只是想公开地址计算硬件以供任意使用,因为他们可以在不使用大量额外晶体管的情况下完成它。解码器必须能够解码寻址模式,并且CPU的其他部分必须能够进行地址计算。将结果放入寄存器而不是将其与段寄存器值一起用于存储器访问并不需要许多额外的晶体管。 Ross Ridge confirms原始8086上的LEA重用CPU有效地址解码和计算硬件。
请注意,大多数现代CPU在与正常添加和转移指令相同的ALU上运行LEA 。它们具有专用的AGU(地址生成单元),但仅用于实际的存储器操作数。有序原子是一个例外; LEA在管道中的运行时间早于ALU:输入必须尽早准备好,但输出也会更快就绪。乱序执行CPU(现代x86的绝大多数)不希望LEA干扰实际的加载/存储,因此它们在ALU上运行。
lea
具有良好的延迟和吞吐量,但在大多数CPU上的吞吐量不如add
或mov r32, imm32
,因此只有在保存指令时才能使用lea
而不是add
。 (见Agner Fog's x86 microarch guide and asm optimization manual。)
内部实现无关紧要,但可以安全地将操作数解码为LEA与任何其他指令的解码寻址模式共享晶体管。 (因此即使在AGU上不能 lea
执行 add
的现代CPU上也存在硬件重用/共享。)任何其他暴露多输入移位和添加的方式指令会对操作数进行特殊编码。
所以386获得了一个移位和添加ALU指令,用于"免费"当它扩展寻址模式以包括scaled-index,并且能够在寻址模式下使用任何寄存器时,LEA也更容易用于非指针。
x86-64可以免费访问程序计数器(instead of needing to read what call
pushed)"免费"通过LEA,因为它增加了RIP相对寻址模式,使得在x86-64位置无关代码中访问静态数据的成本明显低于32位PIC。 (RIP相对确实需要在处理LEA的ALU中提供特殊支持,以及处理实际加载/存储地址的单独AGU。但是不需要新的指令。)
它对于任意算术和指针一样好,所以将它想象为指针这些天是错误的。它不是一种滥用"或"技巧"将它用于非指针,因为所有东西都是汇编语言中的整数。它的吞吐量低于;; 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
,但它足够便宜,几乎所有时间都可以使用,甚至可以保存一条指令。但它最多可以保存三条指令:
esi
在某些AMD CPU上,即使是复杂的LEA也只有2个周期的延迟,但4个指令序列将是从eax
准备就绪到最终lea
准备就绪的4个周期延迟。无论哪种方式,这都会为前端节省3 uop进行解码和发布,并且在重新排序缓冲区中占用空间直到退役。
lea 1(%rdi), %eax
有几个主要好处,特别是在32/64位代码中,寻址模式可以使用任何寄存器并可以移位:
lea (%rdx, %rbp), %ecx
或cmovcc
的复制和添加而有用。lea foo(%rip), %rdi
之前的测试之后可以很方便。或者可能在带有部分标志停顿的CPU上带有加载循环。x86-64:与位置无关的代码可以使用RIP相对LEA 来获取指向静态数据的指针。
7字节mov $foo, %edi
略大且慢于mov r32, imm32
(5字节),因此在符号位于低32位的OS上的位置相关代码中更喜欢mov edi, OFFSET symbol
虚拟地址空间,就像Linux一样。您可能需要disable the default PIE setting in gcc才能使用此功能。
在32位代码中,lea edi, [symbol]
同样比OFFSET
更短,更快。 (在NASM语法中省略lea
。)RIP相对不可用且地址适合32位立即,因此没有理由考虑mov r32, imm32
而不是lea
lea (%rdx, %rbp), %ecx
如果需要将静态符号地址放入寄存器中。
除了x86-64模式中的RIP相对LEA之外,所有这些都适用于计算指针与计算非指针整数加/移。
有关装配指南/手册和性能信息,另请参阅x86 tag wiki。
x86-64的操作数大小与地址大小lea (%rdx, %rbp), %rcx
另见Which 2's complement integer operations can be used without zeroing high bits in the inputs, if only the low part of the result is wanted?。 64位地址大小和32位操作数大小是最紧凑的编码(没有额外的前缀),因此在可能时更喜欢lea (%edx, %ebp), %ecx
而不是64位lea (%edx, %ebp), %ecx
或32位lea (%rdx, %rbp), %ecx
x86-64 lea
总是浪费地址大小前缀与{{1}},但64位地址/操作数大小显然是进行64位数学运算所必需的。 (Agner Fog的objconv反汇编程序甚至警告LEA上使用32位操作数大小的无用地址大小前缀。)
除了Ryzen之外,Agner Fog报告64位模式下的32位操作数大小{{1}}有一个额外的延迟周期。我不知道如果需要将地址大小覆盖到32位,可以在64位模式下加速LEA,如果你需要它截断为32位。
这个问题与高度投票的What's the purpose of the LEA instruction?几乎相同,但大多数答案都是根据实际指针数据的地址计算来解释的。这只是一次使用。
答案 1 :(得分:5)
leaq
没有拥有来操作内存地址,计算一个地址,它实际上并不是读取从结果开始,所以直到mov
之类的人试图使用它,这只是一种神秘的方式来添加一个数字,加上另一个数字的1,2,4或8倍(或者在这种情况下是相同的数字) )。正如你所看到的,它经常被滥用于数学目的。 2*%rdi+%rdi
只是3 * %rdi
,因此它计算x * 3
而不涉及CPU上的乘数单元。
类似地,对于整数,左移位使每个位移位的值加倍(每个零加到右边),这要归功于二进制数的工作方式(十进制数相同的方式,在右边加上零乘以10) )。
所以这是滥用leaq
指令来完成乘法3,然后将结果移位以实现进一步乘以4,最终结果乘以12而不实际使用乘法指令(它大概认为会运行得更慢,而且据我所知,它可能是正确的;猜测编译器通常是一场失败的游戏。
答案 2 :(得分:2)
LEA is for calculating the address。它不会取消引用内存地址
中应该更具可读性m12(long):
lea rax, [rdi+rdi*2]
sal rax, 2
ret
所以第一行相当于rax = rdi*3
然后左移是将rax乘以4,这导致rdi*3*4 = rdi*12