我试图了解地址计算指令的工作原理,尤其是使用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中,这仅仅是内存地址,为什么%rdi乘以3然后左移此内存地址乘以2等于x乘以12?难道不是当我们将%rdi乘以3时,我们跳到另一个不保存值x的内存地址吗?

评论

@Johan,我关闭了stackoverflow.com/questions/13517083/…作为此副本的副本,因为它具有更详细的答案,以消除新手对于将LEA与非指针一起使用的困惑。

相关:LEA指令的目的是什么?大部分是关于lea与mov的问题,而lea与mov是从相反的方向来解决相同的问题。那里所有的答案都涉及将其用于地址/指针,或者只是说“这是移位加法指令的愚蠢名称”,两者都只说明了一半。

#1 楼

leaq不必对内存地址进行运算,它可以计算一个地址,它实际上不会从结果中读取数据,因此,直到mov等尝试使用它时,这只是添加一个数字的深奥方式1、2、4或8倍于另一个数字(在这种情况下为相同数字)。如您所见,出于数学目的,它经常被“滥用”†。 2*%rdi+%rdi只是3 * %rdi,因此它在计算x * 3时不涉及CPU上的乘法器单元。
同样,左移(对于整数)会使移位的每个位的值加倍(向右加零)。二进制数字的工作方式(十进制数字的方法相同,右边的零乘以10)。
因此,这是在滥用leaq指令以将其乘以3,然后将结果移位以进一步乘以4,对于最终乘以12的最终结果,而实际上没有使用过乘法指令(它可能会认为运行速度会更慢,而且据我所知这可能是正确的;再次猜测编译器通常是一场失败的游戏)。 >†:明确一点,它不是滥用,而是以与您的名字所隐含的目的不符的方式使用。以这种方式使用它是100%可以的。

评论


因此,如果我们将x传递为1。假设寄存器为4位,则%rdi将为0001或0x1? (如果我们忽略了long类型)

–通过
17-10-6在2:13



我认为这不是对LEA的滥用,复制和添加是通过lea指令公开CPU的地址生成能力的预期目的之一。看我的答案。

– Peter Cordes
17-10-6在2:43

@ZhiyuanRuan是的,像int / short / long / ...这样的类型在x86-64 ABI中是通过值传递的,在以ABI方式调用某些函数时,值本身在寄存器中。编译器中的原始程序集不涉及任何内存地址。

– Ped7g
17-10-6在4:33

@PeterCordes:“滥用”主要与用于描述指令的术语(加载有效地址)有关;它是为地址生成而设计的,但寄存器是寄存器,两者的数学方法相同。我并不是说使用lea不好,只是说明的名称不会使您认为目的是什么。

–ShadowRanger
17-10-6在10:32

那就是我不同意的地方。我认为它旨在公开硬件的地址生成功能,以用于任意目的。这就是编译器如何思考的,人类也应该如此。命名仅与以下事实有关:它使用寻址模式语法和机器编码,而不是“预期”目的。 (我真的不知道英特尔的想法,正如我在回答中所说的,但是我认为以这种方式向初学者进行解释使使用LEA听起来很正常,因为它是正常的。这就是为什么我不喜欢“滥用”一词的原因“,但这是使用它的合理理由。)

– Peter Cordes
17-10-6在10:40



#2 楼

lea(请参阅英特尔的指令集手册条目)是使用内存操作数语法和机器编码的移位加法指令。这说明了名称,但这并不是唯一的优点。它实际上从不访问内存,因此就像在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]之类的Intel语法代替此问题中使用的AT&T语法。x86机器代码是相同的,而不管您使用哪种语法来创建它。)

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


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

lea具有良好的延迟和吞吐量,但在大多数CPU上的吞吐量不如addmov r32, imm32那么好,因此仅在可以保存指令时才使用lea而不是add。 (请参阅Agner Fog的x86微体系结构指南和asm优化手册。)


内部实现无关紧要,但是可以肯定的是,将操作数解码为LEA的晶体管与任何寻址模式的解码地址相同其他说明。 (因此,即使在未在AGU上执行lea的现代CPU上,也存在硬件重用/共享。)暴露多输入移位加法指令的任何其他方式都将对操作数采用特殊编码。 />
因此,当386扩展了寻址模式以包括缩放索引时,就获得了“免费”的移位加ALU指令,并且能够在寻址模式下使用任何寄存器也使LEA更加易于用于非指针。

x86-64通过LEA“免费”廉价地访问程序计数器(而不需要读取call推送的内容),因为它添加了相对RIP寻址方式,从而可以访问静态数据与32位PIC相比,x86-64位置无关的代码便宜得多。 (RIP相对确实需要特殊的支持来处理LEA的ALU,以及处理实际加载/存储地址的单独的AGU。但是不需要新的指令。)


对任意算术和对指针一样好,因此,如今将其视为针对指针是错误的。将其用于非指针不是“滥用”或“技巧”,因为在汇编语言中,一切都是整数。它的吞吐量低于add,但便宜到足以在保存一条指令时几乎所有时间使用。但是它最多可以保存3条指令:

;; 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), %eaxlea (%rdx, %rbp), %ecx这样的复制和添加操作很有用。

可以在一条指令中执行3或4个操作(见上文)。

在不修改EFLAGS的情况下进行数学运算,可以在cmovcc之前经过测试后很方便。也许在带有部分标志停顿的CPU上的进位加法循环中。

x86-64:位置无关的代码可以使用相对RIP的LEA来获取指向静态数据的指针。 />
7字节的lea foo(%rip), %rdimov $foo, %edi(5字节)稍大且较慢,因此在符号位于虚拟地址空间的低32位的OS(如Linux)上,在位置相关代码中更喜欢mov r32, imm32。您可能需要在gcc中禁用默认PIE设置才能使用此功能。

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

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

请参阅也是x86标记Wiki,以获取组装指南/手册和性能信息。


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反汇编程序甚至警告32位操作数大小的LEA上的地址大小前缀无用。)

除了在Ryzen上,Agner Fog报告在64位模式下的32位操作数大小lea会有额外的延迟周期。我不知道如果需要将地址大小截断为32位,将地址大小覆盖为32位是否可以加快LEA。


这个问题是LEA指令的目的是什么?它几乎是重复的,但是大多数答案都是根据实际指针数据的地址计算来解释它的。那只是一种用途。

#3 楼

LEA用于计算地址。它不会取消引用内存地址

它应该在Intel语法中更具可读性

m12(long):
  lea rax, [rdi+rdi*2]
  sal rax, 2
  ret


所以第一行等效于rax = rdi*3
然后左移将rax乘以4,得到rdi*3*4 = rdi*12