lea
(参见Intel的指令集手册)是一种使用内存操作数语法和机器编码的移位加法指令。这解释了它的名称,但这不是它唯一的用途。它实际上从未访问过内存,因此就像在C中使用&
一样。
例如,请参见如何使用只有2个连续的leal指令在x86上将寄存器乘以37?
在C中,它就像uintptr_t foo = (uintptr_t) &arr[idx]
。请注意,这里使用了&
来给出arr + idx
(由于这是C而不是汇编,因此要缩放arr
的对象大小)。在C中,这将是对语言语法和类型的滥用,但在x86汇编中,指针和整数是相同的东西。一切都只是字节,程序需要按正确顺序放置指令才能获得有用的结果。
有效地址是x86中的一个技术术语:它指代seg:off逻辑地址的“偏移量”部分,特别是在需要执行base_reg + index*scale + displacement
计算时。例如,在%gs:(%rax,%rcx,4)
寻址模式中的rax + (rcx<<2)
。(但EA仍适用于stosb
中的%rdi
, 或movabs
的绝对位移负载/存储,或其他没有ModRM地址模式的情况)。在这种上下文中使用它并不意味着它必须是一个有效/有用的内存地址,而是告诉您该计算不涉及段基址,因此它不会计算一个线性地址。(添加段基址将使它在非平坦内存模型中无法用于实际地址数学计算。)
8086指令集的原始设计师/架构师(Stephen Morse)可能考虑过指针数学作为主要用例,但现代编译器将其视为在指针/整数上进行算术运算的另一个选项,人类也应该这样做。
(请注意,16位寻址模式不包括移位操作,只有[BP | BX] + [SI | DI] + disp8 / disp16
,因此在386之前,LEA对于非指针数学来说并不是那么有用。有关32/64位寻址模式的更多信息,请参见this Q&A,尽管该答案使用Intel语法,例如[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,因此只有在可以使用LEA代替ADD来节省指令时才使用LEA。(参见
Agner Fog的x86微架构指南和asm优化手册和{{link2:https://uops.info/}})Ice Lake为英特尔改进了这一点,现在能够在所有四个ALU端口上运行LEA。
不同微架构下,哪些类型的LEA算法属于“复杂”范畴,只能在较少的端口上运行,规则各不相同。例如,在SnB系列中,3个组件(两个加操作)是速度较慢的情况,而在Ice Lake中,具有缩放指数是低吞吐量的情况。 Alder Lake E-cores(Gracemont)每个时钟周期可处理4个,但只要有索引,就变成了每个时钟周期处理1个,如果有索引和位移(无论是否有基址寄存器),则延迟为2个周期。当存在缩放指数或3个组件时,Zen速度较慢(从每个时钟周期处理4个和1个降至每个时钟周期处理2个和2个周期延迟)。
内部实现是无关紧要的,但可以肯定的是,将操作数解码为LEA与为任何其他指令解码寻址模式共享晶体管。(因此,即使在不执行AGU上的lea的现代CPU上也存在硬件重用/共享。)暴露多输入移位相加指令的任何其他方法都需要对操作数进行特殊编码。
因此,在扩展地址模式以包括缩放索引时,386获得了免费的移位相加ALU指令,并且能够在寻址模式中使用任何寄存器也使LEA更易用于非指针。
通过添加RIP相对寻址模式,x86-64可以便宜地访问程序计数器(而不需要读取call
推送的内容)。这使得在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条指令序列需要4个周期的延迟,从esi准备好到最终eax准备好。无论哪种方式,这都可以节省3个uops的前端解码和发射,并且占用重排序缓冲区中的空间,直到退休。
lea指令有几个主要优点,特别是在32/64位代码中,其中寻址模式可以使用任何寄存器并且可以进行移位:
- 非破坏性:输出在不是输入之一的寄存器中。有时候像
lea 1(%rdi), %eax
或者lea (%rdx, %rbp), %ecx
这样的复制和加法操作很有用。
- 可以在一条指令中执行3或4个操作(见上文)。
- 数学运算不修改EFLAGS,在
cmovcc
之前的测试中可能会很方便。或者在具有部分标志延迟的CPU上进行带进位循环。
- x86-64:位置无关代码可以使用RIP相对LEA来获取静态数据的指针。
7字节的lea foo(%rip), %rdi
比mov $foo, %edi
(5字节)略大且速度较慢,因此在符号位于虚拟地址空间低32位的操作系统(如Linux)上,建议使用mov r32, imm32
来进行位置相关代码。您可能需要disable the default PIE setting in gcc才能使用此功能。
在32位代码中,mov edi, OFFSET symbol
与lea edi, [symbol]
相比同样更短更快。 (在NASM语法中省略OFFSET
。)由于RIP相对地址不可用且地址适合32位立即数,因此如果您需要将静态符号地址加载到寄存器中,则无需考虑使用lea
而不是mov r32, imm32
。
除了x86-64模式下的RIP相对LEA之外,所有这些都同样适用于计算指针与计算非指针整数加法/移位。
另请参阅
x86 <!-->
标签维基,获取汇编指南/手册和性能信息。
x86-64中的操作数大小与地址大小
另请参见哪些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位操作数大小的
lea
具有额外的延迟周期。我不知道如果覆盖地址大小为32位是否可以加快LEA在64位模式下的速度,如果您需要将其截断为32位。
这个问题与
LEA指令的目的是什么?非常相似,但大多数答案都是从实际指针数据的地址计算角度进行解释。那只是其中一种用法。
lea
与mov
的区别,从不同的角度接近了相同的问题。那里的答案都谈到了它用于地址/指针,或者只是说“这是一个愚蠢的名称,用于位移和加法指令”,这些都只讲述了故事的一半。 - Peter Cordes