如果只需要结果的低部分,那么哪个2的补码整数运算可以在input中不调零高位的情况下使用?
在汇编编程中,通常需要从寄存器的低位计算某些值,而不保证其他位为零。 在像C这样的高级语言中,只需简单地将input转换为较小的大小,然后让编译器决定是否需要单独清零每个input的高位,或者是否可以在结束之后剔除结果的高位事实。
对于x86-64(也就是AMD64)来说,这是非常常见的,原因有很多,其中一些出现在其他ISA中。
我将用64位x86作为例子,但其目的是一般性地询问/讨论2的补码和无符号的二进制算术,因为所有现代的CPU都使用它 。 (请注意,C和C ++不保证二进制补码4 ,并且这个有符号溢出是未定义的行为。)
作为例子,考虑一个可以编译成LEA
指令的简单函数2 。 (在x86-64 SysV(Linux) ABI 3中 ,前两个函数参数在rdi
和rsi
,返回值是rax
, int
是32位types。
; 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知道,即使是负符号整数,除了从右到左,input的高位不会影响进入eax
。 因此, 它保存了一个指令字节并使用 lea eax, [rdi + rsi*4 + 3]
还有哪些其他的操作具有这个低位结果的属性,而不依赖于input的高位?
为什么它工作?
脚注
1为什么x86-64经常出现这种情况 :x86-64具有可变长度指令,其中额外的前缀字节会改变操作数的大小(从32到64或16),所以在其他指令中通常可以保存一个字节以相同的速度执行。 当写入寄存器的低8位或16位(或者在稍后读取完整寄存器(Intel pre-IvB))时,它也具有错误依赖性(AMD / P4 / Silvermont):由于历史原因, 只写入32b注册64位寄存器的其余部分为零 。 几乎所有的算术和逻辑都可以在通用寄存器的低8位,16位或32位以及全部64位上使用。 整数向量指令也是非正交的,有些操作对某些元素大小不可用。
此外,与x86-32不同,ABI将寄存器中的函数parameter passing给函数参数,而对于窄types,高位不要求为零。
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]
可以得到相同的结果,它比两个字节短。 因此,地址大小的前缀从来没有用LEA
,因为从Agner Fog的优秀objconv反汇编器反汇编输出的意见警告。
3 x86 ABI :调用者不必为了传递或返回更小的types而使用64位寄存器的上半部分(或符号扩展)。 一个想要使用返回值作为数组索引的调用者必须对它进行签名扩展(使用movzx rax, eax
或者special-case-for-eax指令cdqe
。)(不要与cdq
混淆,将eax
扩展为edx:eax
例如设置为idiv
。))
这意味着返回unsigned int
的函数可以以rax
计算64位临时值的返回值,而不要求mov eax, eax
将 rax
的高位清零 。 这种devise决定在大多数情况下工作良好:通常调用者不需要任何额外的指令来忽略rax
上半部分中未定义的位。
4 C和C ++
C和C ++特别不需要二进制补码二进制符号整数(除了C ++ std::atomic
types )。 补码和符号/幅度也是允许的 ,所以对于完全可移植的C,这些技巧只对unsigned
types有用。 很明显,对于有符号操作,符号/幅度表示中的置位符号位意味着其他位被减去,而不是被加上。 我没有通过补充的逻辑工作
然而, 只有二进制补码才有 的黑客行为是非常普遍的 ,因为实际上没有人关心其他任何事情。 许多使用二进制补码的东西也应该用补码来工作,因为符号位仍然不会改变其他位的解释:它只有 – (2 N -1)(而不是2 N )的值。 符号/幅度表示不具有此属性:根据符号位,每位的位值是正值还是负值。
还要注意,C编译器可以假定签名溢出永远不会发生 ,因为它是未定义的行为。 所以,例如编译器可以假设(x+1) < x
总是假的 。 这使得在C中检测签名溢出相当不方便。 请注意,无符号循环(进位)和有符号溢出之间的区别 。
可用于高位垃圾的宽操作:
- 按位逻辑
- 左移(包括
[reg1 + reg2*scale + disp]
) - 加/减(因此
LEA
指令:地址大小的前缀是不需要的,只要使用所需的操作数大小截断,如果需要的话)。 -
乘法的低一半。 例如16b x 16b→16b可以用32b x 32b→32b完成。 你可以通过使用一个32位的
imul r32, r/m32, imm32
从imul r16, r/m16, imm16
避免LCP失速(和部分寄存器问题) ,然后只读取结果的低16位。 (尽pipe如果使用m32
版本,请注意更宽的内存参考)。正如英特尔的insn ref手册所指出的那样,
imul
的2和3操作数forms对于无符号整数是安全的。 input的符号位不会影响N x N -> N
位乘法结果的N x N -> N
位) - 2 x (即移位
x
):至less在x86上工作,其中移位计数被屏蔽,而不是饱和,直到操作的宽度,所以ecx
高垃圾,甚至是cl
的高位,不影响class次数。 同样适用于BMI2无标记移位(shlx
等),但不适用于向量移位(pslld xmm, xmm/m128
等,使计数饱和)。 智能编译器优化了移位计数的屏蔽,允许在C中进行旋转的安全成语(没有未定义的行为) 。
很明显,像进位/溢出/符号/零的标志都将受到较大操作的高位垃圾的影响。 x86的转换把最后一位移出进位标志,所以这甚至会影响转换。
不能用于高位垃圾的操作:
- 右移
-
完全乘法:例如,对于16b x 16b – > 32b,确保在执行32b x 32b – > 32b
imul
之前,input的高16位为零或符号扩展。 或者使用一个16位的操作数mul
或imul
来不方便地把结果放在dx:ax
。 (有符号和无符号指令的select会影响上16b,就像在32b之前的零或符号扩展一样)。 -
内存寻址(
[rsi + rax]
):符号或零扩展根据需要。 没有[rsi + eax]
寻址模式。 -
除法和余数
- log2 (即最高设定位的位置)
- 跟踪零计数(除非你知道在你想要的部分有一个设置位,或者只是检查一个大于N的结果,因为你没有find检查。)
二进制补码,如无符号二进制数,是一个位值系统。 无符号base2的MSB在N位数(例如2 31 )中的位置值为2 N-1 。 在2的补码中,MSB的值为-2 N-1 (因此作为符号位)。 维基百科的文章解释了许多其他方式来理解2的补码和否定一个无符号的base2数字。
关键在于设置符号位不会改变其他位的解释 。 加法和减法与无符号的base2完全相同,只是结果的解释在有符号和无符号之间有所不同。 (例如, 签名溢出发生在有符号位进位但没有符号位时) 。
另外,进位仅从LSB传播到MSB(从右到左)。 减法是一样的:不pipe高位中是否有任何借位,低位借用它。 如果这导致溢出或进位,只有高位会受到影响。 例如:
0x801F -0x9123 ------- 0xeefc
低8位0xFC
不依赖于他们借用的内容。 他们“环绕”并将借款传递给上层8。
所以加法和减法具有这样的性质:结果的低位不取决于操作数的任何高位。
由于LEA
只使用加法(和左移),所以使用默认的地址大小总是很好。 延迟截断,直到操作数大小进入结果总是很好。
(例外:16位代码可以使用地址大小的前缀来执行32位math运算,在32位或64位代码中,地址大小的前缀会减小宽度,而不是增加。
乘法可以被认为是重复的加法,或者是移位和加法。 低位一半不受高位的影响。 在这个4位的例子中,我已经写出了加到低2结果位的所有位产品。 只涉及任何一个源的低2位。 很明显,这通常是有效的:在加法之前部分产品被移位,所以源中的高位不会影响结果中的低位。
请参阅维基百科,以获得更详细的解释 。 二进制符号乘法有很多不错的google hit ,包括一些教材。
*Warning*: This diagram is probably slightly bogus. ABCD A has a place value of -2^3 = -8 * abcd a has a place value of -2^3 = -8 ------ RRRRrrrr AAAAABCD * d sign-extended partial products + AAAABCD * c + AAABCD * b - AABCD * a (a * A = +2^6, since the negatives cancel) ---------- D*d ^ C*d+D*c
做一个有符号的乘法而不是一个无符号的乘法仍然在低一半 (在这个例子中是低4位) 给出相同的结果 。 部分产品的符号扩展只发生在结果的上半部分。
这个解释并不是很彻底(甚至可能有错误),但是有充分的证据表明在生产代码中使用它是真实和安全的:
-
gcc使用
imul
来计算两个unsigned long
input的unsigned long
imul
。 在Godbolt编译器资源pipe理器中查看gcc的这个例子,利用LEA的其他function 。 -
英特尔的insn ref手册说:
两个和三个操作数forms也可以与无符号操作数一起使用,因为无论操作数是有符号还是无符号,产品的下半部分都是相同的。 然而,CF和OF标志不能用来确定结果的上半部分是否非零。
- 英特尔的devise决定只介绍2个和3个操作数forms的
imul
,而不是mul
。
显然,按位二进制逻辑运算(和/或/ xor / not)独立处理每个位:位位置的结果仅取决于该位位置处的input值。 位移也相当明显。