lea (see Intel's instruction-set manual entry) 是使用内存操作数语法和机器编码的移位加法指令。这解释了名称,但它并不是唯一有用的东西。它实际上从不访问内存,所以它就像在 C 中使用 &。
例如见How to multiply a register by 37 using only 2 consecutive leal instructions in x86?
在 C 中,就像uintptr_t foo = (uintptr_t) &arr[idx]。注意& 给你arr + idx(缩放arr 的对象大小,因为这是C 而不是asm)。在 C 中,这将是对语言语法和类型的滥用,但 在 x86 汇编中指针和整数是一回事。 一切都只是字节,这取决于程序将指令以正确的顺序放置以获得有用的结果。
Effective address 是 x86 中的一个技术术语:它表示 seg:off 逻辑地址的“偏移”部分,尤其是在需要计算 base_reg + index*scale + displacement 时。例如rax + (rcx<<2) 中的 %gs:(%rax,%rcx,4) addressing mode。 (但 EA 仍然适用于 %rdi 的 stosb,或绝对位移 movabs 加载/存储,或其他没有 ModRM addr 模式的情况)。它在这种情况下的使用并不意味着它必须是一个有效/有用的内存地址,它告诉你计算doesn't involve the segment base 所以它不是计算一个线性地址。 (添加 seg 基数会使其无法用于非平面内存模型中的实际地址数学运算。)
8086 指令集 (Stephen Morse) 的原始设计者/架构师可能会或可能不会将指针数学作为主要用例,但 现代编译器认为它只是进行算术的另一种选择关于指针/整数,人类也应该如此。
(请注意,16 位寻址模式不包括移位,仅包含 [BP|BX] + [SI|DI] + disp8/disp16,因此 LEA 对 386 之前的非指针数学没有那样有用。有关更多信息,请参阅 this Q&A关于 32/64 位寻址模式,尽管该答案使用 Intel 语法,如 [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 保存指令时才使用lea 而不是add。 (见Agner Fog's x86 microarch guide and asm optimization manual和https://uops.info/)
Ice Lake 对 Intel 进行了改进,现在可以在所有四个 ALU 端口上运行 LEA。
对于哪种 LEA 是“复杂的”、在较少的端口上运行可以处理它,规则因微架构而异。例如3 分量(两个 + 操作)是 SnB 系列上较慢的情况,具有缩放索引是 Ice Lake 上吞吐量较低的情况。 Alder Lake E-cores (Gracemont) 为 4 个/时钟,但在有索引时为 1 个/时钟,当有索引和位移时为 2 个周期延迟(无论是否有基本寄存器)。当有一个缩放索引或 3 个组件时,Zen 会更慢。 (2c 延迟和 2/clock 从 1c 和 4/clock)。
内部实现无关紧要,但可以肯定的是,将操作数解码为 LEA 与任何其他指令的解码寻址模式共享晶体管。 (因此,即使在现代 CPU 上也存在硬件重用/共享,这些 CPU 不会在 AGU 上执行 lea。)任何其他公开多输入移位和加法指令的方式都会采取操作数的特殊编码。
因此,当 386 扩展寻址模式以包括缩放索引时,它获得了一个“免费”的移位加 ALU 指令,并且能够在寻址模式下使用任何寄存器使得 LEA 更容易用于非指针也是。
x86-64 通过 LEA “免费”获得了对程序计数器 (instead of needing to read what call pushed) 的廉价访问,因为它添加了 RIP 相对寻址模式,使得在 x86-64 位置无关代码中访问静态数据的成本明显低于在 32 位 PIC 中。 (RIP-relative 确实需要在处理 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 微秒,并且一直占用重新排序缓冲区中的空间,直到退休。
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-relative 不可用,并且地址适合 32 位立即数,因此如果您需要获取静态符号,没有理由考虑 lea 而不是 mov r32, imm32地址到寄存器中。
除了 x86-64 模式下的 RIP 相对 LEA 之外,所有这些都同样适用于计算指针与计算非指针整数加法/移位。
另请参阅x86