我试图理解地址计算指令是如何工作的,特别是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的存储器地址?
4条答案
按热度按时间cedebl8k1#
**
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
的对象大小,因为这是C而不是asm)。在C中,这将是对语言语法和类型的滥用,但在x86中,汇编指针和整数是一样的。一切都只是字节,这取决于程序将指令按正确的顺序排列以获得有用的结果。Effective address是x86中的一个技术术语:它表示seg:off逻辑地址的“offset”部分,特别是当需要
base_reg + index*scale + displacement
计算时。例如,%gs:(%rax,%rcx,4)
addressing mode中的rax + (rcx<<2)
。(但EA仍然适用于stosb
的%rdi
,或movabs
加载/存储的绝对位移,或其他没有ModRM addr模式的情况)。它在此上下文中的使用并不意味着它必须是有效/有用的内存地址,它告诉您计算doesn't involve the segment base,因此它不是计算 * 线性 * 地址。(添加seg基址将使其无法用于非平面内存模型中的实际地址数学。)8086指令集的最初设计者/架构师(Stephen莫尔斯)可能会或可能不会将指针数学作为主要用例,但现代编译器认为它只是对指针/整数进行算术的另一种选择,人类也应该如此。
(Note 16位寻址模式不包括移位,只有
[BP|BX] + [SI|DI] + disp8/disp16
,所以莱亚在386之前对非指针数学没有那么有用。有关32/64位寻址模式的更多信息,请参见this Q&A。尽管这个答案使用了Intel的语法,比如[rax + rdi*4]
,而不是这个问题中使用的AT&T语法。x86机器码是相同的,无论您使用什么语法来创建它。)也许8086的设计者们只是想让地址计算硬件可以任意使用,因为他们不需要使用大量的额外晶体管就可以做到这一点。CPU的其他部分必须能够进行地址计算。将结果放入寄存器中,而不是将其与段寄存器值一起用于存储器访问,这是不可能的。Ross Ridge证实,原来8086上的莱亚重用了CPU的有效地址解码和计算硬件。
请注意,大多数现代CPU在与普通加法和移位指令相同的ALU上运行莱亚**。它们有专用的AGU(地址生成单元),但仅用于实际的内存操作数。有序Atom是一个例外;莱亚在流水线中比ALU更早运行:乱序执行CPU(所有现代x86)不希望莱亚干扰实际的加载/存储,因此它们在ALU上运行它。
lea
具有良好的延迟和吞吐量,但在大多数CPU上的吞吐量不如add
或mov r32, imm32
,因此仅在可以使用lea
而不是add
保存指令时使用lea
。(参见Agner Fog's x86 microarch guide and asm optimization manual和https://uops.info/)Ice Lake为Intel改进了这一点,现在能够在所有四个ALU端口上运行莱亚。
对于哪些种类的莱亚是“复杂的”,在可以处理它的较少端口上运行的规则因微架构而异。(两个+操作)是SnB系列上较慢的情况,具有缩放索引是桤木Lake上较低吞吐量的情况。(Gracemont)是4/时钟,但是当有索引时是1/时钟,和2个周期的潜伏期时,有一个索引和位移当有缩放索引或3个组件时,Zen较慢。(2c延迟和2/时钟从1c和4/时钟下降)。
内部实现是无关紧要的,但可以肯定的是,将操作数解码为莱亚与任何其他指令的解码寻址模式共享晶体管。(因此,即使在AGU上不 * 执行 *
lea
的现代CPU上也存在硬件重用/共享。)任何其他暴露多输入移位和加法指令的方式都将对操作数进行特殊编码。因此,当386扩展寻址模式以包括缩放索引时,它得到了一个“自由”的移位和加法ALU指令,并且能够在寻址模式下使用任何寄存器,这使得莱亚也更容易用于非指针。
x86-64可以廉价访问程序计数器(instead of needing to read what
call
pushed)通过莱亚“免费”,因为它增加了RIP相对寻址模式,使得在x86-64位置无关代码中访问静态数据的成本比在32位PIC中低得多。(RIP相对需要处理莱亚的ALU中的特殊支持,以及处理实际加载/存储地址的单独AGU。但不需要新指令。)它对任意算术和指针一样好,所以现在认为它是用于指针的是错误的。将它用于非指针并不是“滥用”或“技巧”,因为在汇编语言中一切都是整数。它的吞吐量比
add
低,但它的成本很低,即使只保存一条指令,也几乎可以一直使用。但它最多可以保存三条指令:在某些AMD CPU上,即使是复杂的莱亚也只有2个周期的延迟,但4指令序列从
esi
准备就绪到最终eax
准备就绪将是4个周期的延迟。无论哪种方式,这都为前端节省了3个uop来解码和发布,并且一直占用重新排序缓冲区中的空间,直到退役。lea
有几个主要优点,特别是在寻址模式可以使用任何寄存器并可以移位的32/64位代码中:*非破坏性:在一个寄存器中的输出不是输入之一。它有时候很有用,就像
lea 1(%rdi), %eax
或lea (%rdx, %rbp), %ecx
一样。*可以在一条指令中执行3或4个操作(见上文)。
*不修改EFLAGS的数学运算,在
cmovcc
之前的测试后可以很方便。或者在具有部分标志暂停的CPU上的加法进位循环中。*x86-64:位置无关代码可以使用RIP相关莱亚来获取指向静态数据的指针。
7-byte
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相关莱亚之外,所有这些都同样适用于计算指针与计算非指针整数加法/移位。
另请参阅x86<!-->tag wiki以获取装配指南/手册和性能信息。
x86-64
lea
的操作数大小与地址大小另请参阅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 (%rdx, %rbp), %ecx
,而不是64位lea (%rdx, %rbp), %rcx
或32位lea (%edx, %ebp), %ecx
。与
lea (%rdx, %rbp), %ecx
相比,x86-64lea (%edx, %ebp), %ecx
总是浪费地址大小前缀,但64位地址/操作数大小显然是进行64位数学运算所必需的。(Agner Fog的objconv反汇编程序甚至警告在具有32位操作数大小的莱亚上使用无用的地址大小前缀。)除了Ryzen,Agner Fog报告说,在64位模式下,32位操作数大小
lea
有一个额外的延迟周期。我不知道如果你需要将地址大小覆盖到32位,是否可以在64位模式下加速莱亚。这个问题几乎与投票率很高的What's the purpose of the LEA instruction?重复,但大多数答案都是从实际指针数据的地址计算方面来解释的。
odopli942#
leaq
不需要对内存地址进行操作,它计算一个地址,它实际上并不从结果中读取,所以在mov
或类似的程序尝试使用它之前,它只是一种将一个数字加上另一个数字的1、2、4或8倍的深奥方法2*%rdi+%rdi
就是3 * %rdi
,所以它在计算x * 3
时不涉及CPU上的乘法器单元。类似地,对于整数,左移每移位一位(向右加一个零)的值就加倍,这要归功于二进制数的工作方式(与十进制数相同,向右加零乘以10)。
因此,这是滥用
leaq
指令来完成乘以3,然后将结果移位以实现进一步乘以4,最终结果是乘以12,而实际上从未使用乘法指令(它可能认为这会运行得更慢,而且据我所知它可能是正确的;第二次猜测编译器通常是失败的游戏)。†:需要明确的是,这不是 * 误用 * 意义上的滥用,只是以一种与你从它的名字中所期望的隐含目的不明确的方式使用它。以这种方式使用它是100%可以的。
dfuffjeb3#
LEA is for calculating the address。它不会取消引用内存地址
在英特尔语法中,它应该更易读
所以第一行相当于
rax = rdi*3
,然后左移是将rax乘以4,结果为rdi*3*4 = rdi*12
hm2xizp94#
我认为出现混淆是因为第一个操作数
(%rdi, %rdi, 2)
看起来像一个内存引用。从书 * 计算机系统:A Programmer's Perspective* by Randal Bryant and大卫O'Hallaron关于
leaq
:它的第一个操作数似乎是一个内存引用,但不是从指定的位置阅读,而是将有效地址复制到目的地。
下面是相关部分:
此指令可用于为以后的内存引用生成指针。此外,它还可用于简洁地描述常见的算术运算。例如,如果寄存器
rdx
包含值x
,则指令leaq 7(%rdx,%rdx, 4) , %rax
将寄存器%rax
设置为5X+7。编译器经常发现leaq
的巧妙用法,这些用法与有效的地址计算无关。