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

Using LEA on values that aren't addresses / pointers?

我试图了解地址计算指令的工作原理,尤其是 leaq 命令。然后,当我看到使用 leaq 进行算术计算的示例时,我会感到困惑。比如下面的C代码,

long m12(long x) {
return x*12;
}

在汇编中,

leaq (%rdi, %rdi, 2), %rax
salq , $rax

如果我的理解是正确的,leaq 应该移动任何地址 (%rdi, %rdi, 2),应该是 2*%rdi+%rdi,评估为 %rax。我感到困惑的是,因为值 x 存储在 %rdi 中,这只是内存地址,为什么 times %rdi by 3 然后左移这个 memory address by 2 等于到 x 乘以 12?是不是当我们将 %rdi 乘以 3 时,我们跳转到另一个不保存值 x 的内存地址?

leaq 没有 来操作内存地址,它 计算 一个地址,它没有实际上 read 从结果中,所以直到 mov 或类似的人尝试使用它,它只是一种添加一个数字的深奥方法,加上 1、2、4 或 8乘以另一个数字(或本例中的相同数字)。如您所见,出于数学目的,它经常被“滥用”2*%rdi+%rdi 只是 3 * %rdi,因此它计算 x * 3 而不涉及 CPU 上的乘法器单元。

同样,对于整数,左移,每移动一位(每向右添加一个零),值就会加倍,这要归功于二进制数的工作方式(与十进制数相同,在右边加零乘以10).

所以这是在滥用 leaq 指令来完成乘以 3,然后将结果移位以实现进一步乘以 4,最终结果是乘以 12 而没有实际使用乘法指令(它大概认为 运行 会更慢,据我所知这可能是正确的;事后猜测编译器通常是一场失败的游戏)。

:需要明确的是,这不是滥用意义上的滥用,只是以不被滥用的方式使用它清楚地符合您从其名称中所期望的隐含目的。这样用100%没问题

LEA is for calculating the address。它不会取消引用内存地址

Intel syntax

中应该更具可读性
m12(long):
  lea rax, [rdi+rdi*2]
  sal rax, 2
  ret

所以第一行相当于rax = rdi*3 那么左移就是rax乘以4,得到rdi*3*4 = rdi*12

lea (see Intel's instruction-set manual entry) 是一个使用内存操作数语法和机器编码的移位加指令。这解释了这个名字,但这并不是它唯一的用处。 它实际上从不访问内存,所以它就像在 C 中使用 &

参见示例

在 C 中,类似于 uintptr_t foo = (uintptr_t) &arr[idx]。请注意 & 为您提供 arr + idx(缩放 arr 的对象大小,因为这是 C 而不是 asm)。在 C 中,这会滥用语言语法和类型,但 在 x86 中,汇编指针和整数是一回事。 一切都只是字节,这取决于程序将指令放入获得有用结果的正确顺序。

is a technical term in x86: it means the "offset" part of a seg:off logical address, especially when a base_reg + index*scale + displacement calculation was needed. e.g. the rax + (rcx<<2) in a %gs:(%rax,%rcx,4) . (But EA still applies to %rdi for stosb, or the absolute displacement for movabs load/store, or other cases without a ModRM addr mode). Its use in this context doesn't mean it must be a valid / useful memory address, it's telling you that the calculation 所以它不计算 线性 地址。 (添加 seg 基数会使它无法用于非平面内存模型中的实际地址数学运算。)


8086 指令集 (Stephen Morse) 的原始设计者/架构师可能会或可能不会将指针数学作为主要用例,但 现代编译器将其视为只是对指针/整数进行算术运算的另一种选择,人类也应该如此。

(请注意,16 位寻址模式不包括移位,仅包括 [BP|BX] + [SI|DI] + disp8/disp16,因此 LEA 不像 对 386 之前的非指针数学有用. 有关 32/64 位寻址模式的更多信息,请参阅 ,尽管该答案使用像 [rax + rdi*4] 这样的 Intel 语法,而不是此问题中使用的 AT&T 语法。x86 机器代码是相同的,无论什么语法你用来创建它。)

也许 8086 架构师只是想公开地址计算硬件以供任意使用,因为他们可以在不使用大量额外晶体管的情况下做到这一点。解码器已经必须能够解码寻址模式,并且 CPU 的其他部分必须能够进行地址计算。将结果放在寄存器中而不是将其与用于内存访问的段寄存器值一起使用不会占用很多额外的晶体管。 Ross Ridge confirms 原始 8086 上的 LEA 重用了 CPU 的有效地址解码和计算硬件。


请注意,大多数 现代 CPUs 运行 LEA 在与普通加法和移位指令相同的 ALU 上 。它们有专用的 AGU(地址生成单元),但仅将它们用于实际的内存操作数。 In-order Atom 是一个例外; LEA 运行s 在流水线中比 ALU 早:输入必须更快准备好,但输出也必须更快准备好。乱序执行 CPUs(所有现代 x86)不希望 LEA 干扰实际 loads/stores 所以他们 运行 它在 ALU 上。

lea 具有良好的延迟和吞吐量,但在大多数 CPU 上的吞吐量不如 addmov r32, imm32,因此仅使用 lea当你可以用它而不是 add 保存指令时。 (参见 Agner Fog's x86 microarch guide and asm optimization manual and https://uops.info/
Ice Lake 针对 Intel 进行了改进,现在能够在所有四个 ALU 端口上 运行 LEA。

关于哪种类型的 LEA 的规则是“复杂的”,运行宁可处理它的端口较少,因微体系结构而异。例如3 分量(两个 + 操作)是 SnB 系列上较慢的情况,具有缩放索引是 Ice Lake 上吞吐量较低的情况。 Alder Lake E-cores (Gracemont) 是 4/clock,但是当有索引时是 1/clock,当有索引和位移(无论是否有 base reg)时是 2-cycle 延迟。当有缩放索引或 3 个组件时,Zen 会变慢。 (2c 延迟和 2/clock 从 1c 和 4/clock 下降)。


内部实现无关紧要,但可以肯定的是,将操作数解码为 LEA 与任何其他指令的解码寻址模式共享晶体管。 (因此,即使在现代 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 以及处理实际 load/store 地址的独立 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 CPUs 上,即使是复杂的 LEA 也只有 2 个周期的延迟,但是 4 条指令序列从 esi 准备到最后的 [=34] 将是 4 个周期的延迟=] 准备好了。无论哪种方式,这都为前端解码和发布节省了 3 微码,并且在重新排序缓冲区中一直占用 space 直到退休。

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

  • 非破坏性:在不是输入之一的寄存器中输出。它有时很有用,就像 lea 1(%rdi), %eaxlea (%rdx, %rbp), %ecx.
  • 这样的复制和添加
  • 可以在一条指令中执行 3 或 4 个操作(见上文)。
  • Math不修改EFLAGS,考前可以得心应手cmovcc。或者可能在 CPUs 的 add-with-carry 循环中带有部分标志停顿。
  • x86-64: 位置独立代码可以使用 RIP-relative LEA 获取指向静态数据的指针。

7 字节 lea foo(%rip), %rdimov $foo, %edi(5 字节)稍大且慢,因此在符号位于低位 32 的操作系统上,在位置相关代码中更喜欢 mov r32, imm32虚拟地址位 space,如 Linux。您可能需要 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 之外,所有这些都同样适用于计算指针与计算非指针整数加法/移位。

另请参阅 <!--> tag wiki 以获取组装指南/手册和性能信息。


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

另见 。 64 位地址大小和 32 位操作数大小是最紧凑的编码(没有额外的前缀),因此尽可能使用 lea (%rdx, %rbp), %ecx 而不是 64 位 lea (%rdx, %rbp), %rcx 或 32 位 lea (%edx, %ebp), %ecx .

lea (%rdx, %rbp), %ecx 相比,

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

除了可能在 Ryzen 上,Agner Fog 报告说 64 位模式下的 32 位操作数大小 lea 有一个额外的延迟周期。我不知道将地址大小覆盖为 32 位是否可以在 64 位模式下加速 LEA,如果你需要它 t运行cate 到 32 位。


这个问题几乎与得票率很高的问题 What's the purpose of the LEA instruction? 重复,但大多数答案都根据实际指针数据的地址计算来解释它。那只是一个用途。