这在x86-64(也称为AMD64)中特别常见,原因有很多1,其中一些也存在于其他ISA中。
我将使用64位x86作为示例,但意图是讨论二进制补码和无符号二进制算术,因为所有现代CPU都使用它。(请注意,C和C ++不保证二进制补码4,且有符号溢出是未定义行为。)
作为一个例子,考虑一个可以编译成
LEA
指令的简单函数2。(在x86-64 SysV(Linux)ABI3中,前两个函数参数位于rdi
和rsi
中,返回值在rax
中。int
是一个32位类型。); int intfunc(int a, int b) { return a + b*4 + 3; }
intfunc:
lea eax, [edi + esi*4 + 3] ; the obvious choice, but gcc can do better
ret
gcc知道即使是负有符号整数的加法也只会从右到左进位,因此输入的高位不能影响进入eax
的内容。因此,它节省了一字节指令并使用了lea eax,[rdi + rsi * 4 + 3]
还有哪些操作具有结果的低位与输入的高位无关的特性?
为什么这样做有效呢?
脚注
1 为什么这在x86-64中经常出现: x86-64具有可变长度指令,其中额外的前缀字节会改变操作数大小(从32位到64位或16位),因此在执行速度相同的指令中通常可以节省一个字节。当写入寄存器的低8位或16位时(AMD/P4/Silvermont)存在假依赖性,或者在以后读取完整寄存器时存在停顿(Intel pre-IvB):由于历史原因,仅向32位子寄存器写入会将其余64位寄存器清零。几乎所有算术和逻辑都可以用于通用寄存器的低8、16或32位以及完整的64位。整数矢量指令也相当不正交,某些元素大小的一些操作不可用。
此外,与x86-32不同,ABI在寄存器中传递函数参数,并且对于窄类型,不需要将高位设置为零。
2 LEA: 与其他指令一样,LEA 的默认操作数大小为32位,但默认地址大小为64位。操作数大小前缀字节(0x66
或REX.W
)可以使输出操作数大小为16位或64位。地址大小前缀字节(0x67
)可以将地址大小减小到32位(在64位模式下)或16位(在32位模式下)。因此,在64位模式下,lea eax,[edx + esi]
比lea eax,[rdx + rsi]
多占用一个字节。
可以使用lea rax,[edx + esi]
,但地址仍然只使用32位计算(进位不会设置rax
的第32位)。您可以使用lea eax,[rdx + rsi]
获得相同的结果,它比前者短两个字节。因此,在Agner Fog的出色objconv反汇编输出中的注释中警告,地址大小前缀永远没有用处。
3 x86 ABI:调用方不必将用于传递或返回较小类型的64位寄存器的上半部分清零(或符号扩展)。希望将返回值用作数组索引的调用者必须对其进行符号扩展(使用movzx rax,eax
或特殊情况下针对eax的指令cdqe
。(不要与将eax
符号扩展为edx:eax
的cdq
混淆,例如为了设置idiv
而进行的操作。)
unsigned int
的函数可以在一个64位的临时变量 rax
中计算其返回值,而不需要 mov eax, eax
来清零 rax
的高位。这个设计决策在大多数情况下都很有效:通常调用者不需要任何额外的指令来忽略 rax
上半部分的未定义位。
4 C和C++
C和C++不需要二进制补码有符号整数(除了C++ std::atomic
类型)。 补码反码和原码也是允许的, 所以对于完全可移植的C来说,这些技巧只对unsigned
类型有用。显然,对于有符号操作,补码表示中的设置符号位意味着其他位被减去,而不是相加,例如。 我还没有研究过反码的逻辑。
此外请注意,C编译器可以假设有符号溢出永远不会发生,因为这是未定义的行为。例如编译器可能会并且确实假定
(x+1) < x
总是为false。这使得在C中检测有符号溢出变得相当麻烦。请注意无符号环绕(进位)和有符号溢出之间的区别。