我试图理解地址计算指令是如何工作的,尤其是使用< code>leaq命令。然后,当我看到使用< code>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的内存地址吗?
LEA用于计算地址。它不会解除对内存地址的引用
在英特尔语法中,它应该更可读
m12(long):
lea rax, [rdi+rdi*2]
sal rax, 2
ret
所以第一行等价于 rax = rdi*3
然后左移是将 rax 乘以 4,得到 rdi*3*4 = rdi*12
<code>leaq</code>不需要对内存地址进行运算,它计算一个地址,它实际上不会从结果中读取,因此,除非<code>mov</code>或类似程序尝试使用它,否则它只是一种深奥的方法,将一个数字加上另一个数字的1、2、4或8倍(在本例中是相同的数字)。如您所见,它经常被“滥用”†用于数学目的2*%rdi%rdi
只是3*%rdi
,因此它计算x*3
,而不涉及CPU上的乘法器单元。
同样,对于整数,左移会使每位移位的值翻倍(每增加一个零到右边),这要归功于二进制数的工作方式(与十进制数相同,在右边增加零乘以10)。
因此,这是滥用leaq
指令来完成乘以3的乘法,然后将结果移动以实现进一步乘以4,以获得乘以12的最终结果,而无需实际使用乘法指令(它大概认为会运行得更慢,据我所知,这可能是正确的;事后猜测编译器通常是一场失败的游戏)。
†:需要明确的是,它不是滥用意义上的滥用,只是以一种与你对它的名字所期望的隐含目的不明显一致的方式使用它。以这种方式使用它是100%可以的。
lea
(请参阅英特尔指令集手册条目)是一条使用内存操作数语法和机器编码的移位和加法指令。这就解释了它的名字,但它不是唯一有用的东西。它实际上从不访问内存,所以它就像使用<code>一样
例如,请参阅如何在x86中仅使用两个连续的leal指令将寄存器乘以37?
在C中,它类似于<code>uintptr_t foo=(uintpt_t)
有效地址是 x86 中的一个技术术语:它表示 seg:off 逻辑地址的“偏移”部分,特别是当需要base_reg索引*尺度位移
计算时。例如 rax (rcx
8086指令集的最初设计者/架构师(Stephen Morse)可能会或可能不会将指针数学作为主要用例,但现代编译器认为它只是对指针/整数进行算术运算的另一种选择,人类也应该如此。
(注意16位寻址模式不包括移位,只有< code >[BP | BX][SI | DI]disp 8/disp 16
,所以LEA在386之前对非指针数学没那么有用。看到这个Q
也许8086的设计者只是想公开地址计算硬件以供任意使用,因为他们不需要使用很多额外的晶体管。解码器必须能够解码寻址模式,CPU的其他部分必须能够进行地址计算。将结果放入寄存器,而不是与段寄存器值一起用于内存访问,并不需要很多额外的晶体管。Ross Ridge确认LEA on original 8086重用了CPU有效地址解码和计算硬件。
请注意,大多数现代CPU在与普通添加和移位指令相同的ALU上运行LEA。它们有专用的AGU(地址生成单元),但仅将它们用于实际的内存操作数。有序Atom是一个例外;LEA在管道中运行的时间比ALU早:输入必须更快准备好,但输出也更快准备好。乱序执行CPU(所有现代x86)不希望LEA干扰实际加载/存储,因此它们在ALU上运行它。
lea
具有良好的延迟和吞吐量,但吞吐量不如大多数CPU上的add
或mov r32, im32
,因此只有在可以保存指令时才使用lea
而不是add
。(请参阅Agner Fog的x86微拱指南和ash优化手册和https://uops.info/)
冰湖改进了英特尔,现在能够在所有四个ALU端口上运行LEA。
对于哪种 LEA 是“复杂的”,在较少的可以处理它的端口上运行的规则因微体系结构而异。例如,3分量(两个操作)是SnB系列上较慢的情况,具有缩放索引的是Ice Lake上的较低吞吐量情况。Alder Lake E-cores(Gracemont)是4个/时钟,但是当有索引时是1个/时钟,当有索引和位移时,则是2个周期的延迟(无论是否有基本reg)。当有一个缩放索引或3个分量时,禅宗会变慢。(2c 延迟和 2/时钟从 1c 和 4/时钟下降)。
内部实现无关紧要,但可以肯定的是,将操作数解码为LEA与任何其他指令的解码寻址模式共享晶体管。(因此,即使在不在AGU上执行lea
的现代CPU上也存在硬件重用/共享。)任何其他公开多输入移位和添加指令的方式都会对操作数进行特殊编码。
因此,当386扩展寻址模式以包括按比例变址时,它得到了一个“自由”的移位和加法ALU指令,并且能够在寻址模式中使用任何寄存器也使得LEA更容易用于非指针。
x86-64通过LEA“免费”获得了对程序计数器的廉价访问(而不是需要读取< code>call推送的内容),因为它添加了RIP相关寻址模式,使得在x86-64位置无关代码中访问静态数据比在32位PIC中便宜得多。(RIP-relative确实需要处理LEA的alu以及处理实际加载/存储地址的单独agu的特殊支持。但是不需要新的指令。)
它对于任意算术和指针一样好,所以现在认为它是用于指针是错误的。将其用于非指针并不是“滥用”或“欺骗”,因为在汇编语言中,一切都是整数。它的吞吐量低于<code>add</code>,但它非常便宜,几乎可以在保存一条指令时随时使用。但它最多可以保存三条指令:
;; 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个周期的延迟,但从<code>esi
lea
有几个主要优点,特别是在32/64位代码中,其中寻址模式可以使用任何寄存器并可以移位:
在32位代码中,< code>mov edi,OFFSET symbol同样比< code>lea edi,[symbol]更短更快。(省略NASM语法中的< code>OFFSET。)RIP-relative不可用,地址适合32位立即数,因此如果需要将静态符号地址放入寄存器,没有理由考虑< code>lea而不是< code>mov r32,imm32。
除了x86-64模式中的RIP相对LEA之外,所有这些都同样适用于计算指针和计算非指针整数相加/移位。
另请参见x86
x86-64的操作数大小与地址大小lea
除了可能在Ryzen上,Agner Fog报告说,64位模式下的32位操作数大小<code>lea</code>具有额外的延迟周期。我不知道在64位模式下,如果需要将地址大小重写为32位,是否可以加快LEA的速度。
这个问题几乎是投票率很高的LEA指令的目的的重复?但大多数答案都是从实际指针数据的地址计算的角度来解释它。这只是一种用途。
///01.地址.c #include <stdio.h> #include <stdlib.h> int get() { return 10; } //01.严格进行变量区分: // 1.普通变量和指针变量 // 2.严格的变量类型: // 常规类型+特殊类型 //02.对变量取地址的操作发起于寄存器当中 // 因此地址也生成于寄存器变量当中,C语言无法直接访问 //
问题1. 为什么将原生指针放到智能指针里后,再通过 get()取出来的地址和原生指针地址不同呢? 比如例子中打印的base0和 base2不同。 问题2. 为什么将 nullptr 放到智能指针里后,通过 get()取出来的地址不是 nullptr 呢? 那如果判断智能指针管理的原生指针是否为 nullptr呢? 输出: base0=0x156704080 base1=0x0 base2=0x15
本文向大家介绍指针与地址的区别? 相关面试题,主要包含被问及指针与地址的区别? 时的应答技巧和注意事项,需要的朋友参考一下 区别: 1指针意味着已经有一个指针变量存在,他的值是一个地址,指针变量本身也存放在一个长度为四个字节的地址当中,而地址概念本身并不代表有任何变量存在. 2 指针的值,如果没有限制,通常是可以变化的,也可以指向另外一个地址. 地址表示内存空间的一个位置点,他是用来赋给指针的,地
问题内容: 在Go 规范中写道: 字符串是不可变的:一旦创建,就无法更改字符串的内容。 我有以下代码: 我原本希望地址在之后更改。就像Java那样,我们在其中重新分配String引用。 什么是“不变性”? 问题答案: 值 是不可变的。 不是一个值。这是一个 变量 (类型)。并且变量的值可能会更改,这就是您对任何编程语言所期望的。 是一个值,这是不可变的。是另一个值,当您分配给时,您只需为变量分配另
问题内容: 我只是在玩Go,还没有关于何时按值或按引用传递结构的良好心理模型。 这可能是一个非常愚蠢的问题,但我只想尝试一下,看看我是否仍在处理同一对象,或者是否已对其进行了复制(按值传递)。 有没有一种方法可以打印对象的指针(如果gc更改了指针值,则为内部ID)? 在我的Windows上给出(8g编译版本): 为什么go例程中的指针值显示不同的值?原始对象上的数量确实发生了更改,因此它正在使用同
问题内容: 让我们说我有以下代码: 我使用此函数打开文件,然后从执行其他活动的另一个函数调用该函数。 我的问题是,既然我已经打开文件,如何关闭它。如果要在里面添加文件,在返回之前是否会关闭文件?在调用函数中使用defer是否有意义? 问题答案: 如果函数的目的是返回文件,为什么要在返回文件的函数中将其关闭? 在这种情况下, 调用者 有责任正确关闭文件,最好使用: 尽管您的函数吞没了错误,但是您应该