在不是地址/指针的值上使用LEA?

我试图理解地址计算指令是如何工作的,特别是使用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的内存地址?

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语法。不管用什么语法创建它,x86机器代码都是相同的。 )

也许8086架构师只是希望将地址计算硬件暴露给任意用途,因为他们可以在不使用大量额外晶体管的情况下完成它。 解码器必须能够解码寻址模式,并且CPU的其他部分必须能够进行地址计算。 将结果放入寄存器而不是使用段寄存器值进行存储器访问不需要额外的晶体管。 Ross Ridge确认原始8086上的LEA重用了CPU的有效地址解码和计算硬件。


请注意,大多数现代CPU在与正常添加和移位指令相同的ALU上运行LEA 。 它们具有专用的AGU(地址生成单元),但仅用于实际的存储器操作数。 有序Atom是一个例外; LEA在管道中的运行时间早于ALU:输入必须尽快准备好,但输出也会更快就绪。 乱序执行CPU(现代x86的绝大多数)不希望LEA干扰实际的加载/存储,因此它们在ALU上运行它。

lea具有良好的延迟和吞吐量,但在大多数CPU上没有像add或移动mov r32, imm32那样好的吞吐量,所以只有在用它保存指令而不是add时才使用lea 。 (参见Agner Fog的x86微指南指南和asm优化手册 。)


内部实现是无关紧要的,但是可以安全地将操作数解码为LEA与任何其他指令的解码寻址模式共享晶体管 。 (因此即使在不在AGU上执行 lea现代CPU上也存在硬件重用/共享。)任何其他暴露多输入移位和加法指令的方式都会对操作数采取特殊编码。

因此386在扩展寻址模式以包括缩放索引时获得了“免费”的移位和添加ALU指令,并且能够在寻址模式中使用任何寄存器使LEA更容易用于非指针。 。

x86-64通过LEA“免费”获得了对程序计数器的便宜访问( 而不需要读取推送的内容 ),因为它增加了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 uop进行解码和发布,并且在重新排序缓冲区中一直占用空间直到退役。

lea有几个主要的好处 ,特别是在32/64位代码中,寻址模式可以使用任何寄存器并且可以移位:

  • 非破坏性:寄存器中的输出不是输入之一 。 它有时只是像lea 1(%rdi), %eaxlea (%rdx, %rbp), %ecx这样的复制和添加。
  • 可以在一条指令中执行3或4次操作 (见上文)。
  • 没有修改EFLAGS的数学 ,在cmovcc之前的测试之后可以很方便。 或者可能在具有部分标志停顿的CPU上的加载循环中。
  • x86-64:位置无关代码可以使用RIP相对LEA来获取指向静态数据的指针。

    7字节lea foo(%rip), %rdimov $foo, %edi (5字节)略大mov $foo, %edi ,所以在符号位于低32位的操作系统上更喜欢位置相关代码中的mov r32, imm32虚拟地址空间,如Linux。 您可能需要在gcc中禁用默认PIE设置才能使用它。

    在32位代码中, mov edi, OFFSET symbol同样比lea edi, [symbol]更短更快。 (在NASM语法中省略OFFSET 。)RIP相对不可用且地址适合32位立即数,因此如果需要将静态符号地址输入寄存器,则没有理由考虑lea而不是mov r32, imm32

除了x86-64模式中的RIP相对LEA之外,所有这些同样适用于计算指针与计算非指针整数加/移。

另请参阅x86 标签wiki以获取assembly指南/手册和性能信息。


x86-64 lea操作数大小与地址大小

另请参见如果只需要结果的低部分,可以使用哪个2的补码整数运算而不将输入中的高位归零? 。 64位地址大小和32位操作数大小是最紧凑的编码(没有额外的前缀),所以更喜欢lea (%rdx, %rbp), %ecx ,而不是64位lea (%rdx, %rbp), %rcx或32位lea (%edx, %ebp), %ecx

x86-64 lea (%edx, %ebp), %ecx总是浪费地址大小前缀与lea (%rdx, %rbp), %ecx ,但显然需要64位地址/操作数大小做64位数学。 (Agner Fog的objconv反汇编程序甚至警告LEA上使用32位操作数大小的无用地址大小前缀。)

除了Ryzen之外,Agner Fog报告说64位模式下的32位操作数大小有一个额外的延迟周期。 我不知道如果需要将地址大小覆盖到32位,可以在64位模式下加速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