C 语言中的哪些运算符会产生汇编命令sal, shl, sar or shr
,例如?
哪些运算符使用 sal、shl、sar 或 shr
首先应该注意的是,有很多架构,每个架构都有自己的指令集。我假设你的意思是x86(并且您确实应该将正确的架构标记为上面所说的 0xC0000022L)。以下答案的大部分内容也适用于其他架构,但它们可能使用不同的助记符或缺少一些提到的说明
SAL
并且SHL
是一样的。它们只是相同操作码的别名,因为左移总是用 0s 填充空位。在 C 中<<
会做左移,而移位指令是打印为SAL还是SHL取决于编译器/反汇编器
OTOH 有两种右移版本,因为您可以用零(逻辑移位)或旧值的高位(算术移位)填充移出的位。SAR
进行算术移位并SHR
进行逻辑移位。在 C 中,右移运算符是>>
,但规则取决于类型的符号:
- 无符号类型的右移始终是逻辑移位,因此
SHR
将使用 - 有符号类型的右移是实现定义的,即编译器可以选择进行算术或逻辑移位。然而,几乎所有现代编译器都会
SAR
对有符号类型进行算术移位 ( ) (否则进行算术移位会太棘手/笨拙)。一些编译器可能有一个选项来选择右移变体
根据C99 标准,第 6.5.7 节:
对每个操作数执行整数提升。结果的类型是提升的左操作数的类型。如果右操作数的值为负或大于或等于提升的左操作数的宽度,则行为未定义。
E1 << E2的结果是E1左移E2位位置;空出的位用零填充。如果E1具有无符号类型,则结果的值为E1 × 2 E2,比结果类型中可表示的最大值减少模 1。如果E1具有有符号类型和非负值,并且E1 × 2 E2在结果类型中可表示,则这就是结果值;否则,行为未定义。
E1 >> E2的结果是E1右移E2位位置。如果E1具有无符号类型或者如果E1具有有符号类型和非负值,则结果的值是E1 / 2 E2的商的整数部分。如果E1具有有符号类型和负值,则结果值是实现定义的。
然而,还有很多其他操作可以产生移位指令,以及移位运算符不产生移位指令的各种情况
不太常见的<</>>
是未编译为 shift 指令,但编译器可能会优化x << 1
为x += x
,您会看到诸如ADD eax, eax
或 之类的东西LEA ecx, [eax + eax]
。在 x86 上x << i
, i ⩽ 3 也可以编译为LEA eax, [eax*2ⁱ]
而不是移位。当然,MUL x, 2
在没有移位或移位比乘法慢的假设架构上也可以得到类似的输出
编译器还能够将复杂的语句(x << 1) + (x << 4) + (x << 13)
转换为更简单的语句,例如单次乘以 8210,不再需要移位。
或者 GCC 识别(a ^ b) + (a & b) + (a & b)
及其逆条件(a + b) - (a & b) - (a & b)
并将它们分别优化为a + b
和a ^ b
,因此(在未来)它们有可能将等价物(a ^ b) + ((a & b) << 1)
和(a + b) - ((a & b) << 1)
转换为ADD
和 ,XOR
而根本没有任何转换。
看到他们在行动
对于另一种情况,有各种示例:
- 乘以 2 的幂是通过左移完成的。在 x86 上存在更通用的
LEA
指令,因此对于指数 ⩽ 8 之间的选择LEA
和SHL
取决于编译器。数组运算也需要大量的乘法,所以通常也使用移位 - 乘以许多其他常数也可以优化为一系列 ADD/SUB 和移位,如果这比
MUL
指令本身快的话。再次在 x86 中偶尔使用 LEA 代替移位 - 除以 2 的幂是通过右移完成的。对于无符号类型,这是一个简单的逻辑转换。对于有符号类型,它是一个算术移位,然后是一些其他移位和 ADD 以更正结果(因为除法向零舍入,而算术右移向 -inf 舍入)
- 除以常数将被优化为乘以相应乘法逆的乘法,这可能涉及一些移位以四舍五入结果。
SHR
如果从 Sandy Bridge 开始调整微体系结构,Clang 甚至会发出一个检查高位的信号,同时通过一个非常数进行除法- 位域访问当然需要在架构中使用大量移位,而没有像 x86 这样的高效位域操作。看演示
- ...
以下是 mul/div 示例的一些插图。你可以很容易地看到x*15
被替换x*16 - x
,并x*33
通过完成x*32 + x
,即(x << 4) - x
和(x << 5) + x
。此外,x*8
针对编译器进行优化lea eax, [0+rdi*8]
或shl edi, 3
取决于编译器。助记符SAL
和SHL
也由编译器自由选择
我还放了一些非 x86 编译器进行比较,因为它们没有LEA
但可能有其他与移位相关的指令或除了正常的移位指令之外的不同移位功能。您可以在各种 x86 和非 x86 编译器之间进行更改,以查看其输出之间的差异。另一个结合了我上面所说的多项内容的示例:
struct bitfield {
int x: 10;
int y: 12;
int z: 10;
};
int f(bitfield b)
{
int i = b.x*65;
int j = b.y/25;
int k = b.z/8;
return (i << j) + (k >> j);
}
f(bitfield):
mov eax, edi
mov edx, edi
sar edi, 22
sal eax, 10
sal edx, 6
sar eax, 20
sar dx, 6
imul ecx, eax, 5243
sar ax, 15
sar ecx, 17
sub ecx, eax
movsx eax, dx
mov edx, eax
movsx ecx, cx
sal edx, 6
add edx, eax
lea eax, [rdi+7]
sal edx, cl
test di, di
cmovns eax, edi
sar ax, 3
cwde
sar eax, cl
add eax, edx
ret
你可以打开Godbolt链接,看看哪条指令对应哪一行彩色代码
总结:现在的编译器真的很聪明,可以向普通人输出“令人惊讶”的结果。他们几乎可以为 C 中的任何运算符发出移位指令。使用优化编译器,一切都结束了
也可以看看
int main (void){
unsigned int uin = 0x1000;
signed int sin = -0x1000;
return (uin<<8)+(uin>>8)+(sin<<8)+(sin>>8);
}
编译并链接到
cl /Zi /W4 /Od /analyze /nologo salsaar.cpp /link /release
拆解
:\>cdb -c "uf salsaar!main;q" salsaar.exe | grep -A 20 Reading
0:000> cdb: Reading initial command 'uf salsaar!main;q'
salsaar!main:
01121000 55 push ebp
01121001 8bec mov ebp,esp
01121003 83ec08 sub esp,8
01121006 c745fc00100000 mov dword ptr [ebp-4],1000h
0112100d c745f800f0ffff mov dword ptr [ebp-8],0FFFFF000h
01121014 8b45fc mov eax,dword ptr [ebp-4]
01121017 c1e008 shl eax,8
0112101a 8b4dfc mov ecx,dword ptr [ebp-4]
0112101d c1e908 shr ecx,8
01121020 03c1 add eax,ecx
01121022 8b55f8 mov edx,dword ptr [ebp-8]
01121025 c1e208 shl edx,8
01121028 03c2 add eax,edx
0112102a 8b4df8 mov ecx,dword ptr [ebp-8]
0112102d c1f908 sar ecx,8
01121030 03c1 add eax,ecx
01121032 8be5 mov esp,ebp
01121034 5d pop ebp
01121035 c3 ret
注意 shl 和 sal 都是相同的(操作码相同并且工作相同)由于有符号的无符号差异,shr 和 sar 不同