我发誓,我不想优化任何事情,我只是出于好奇而问这个问题。我知道,在大多数硬件有位移(例如的组件的命令shl
,shr
),它是一个命令。但这有多少关系(纳秒级或CPU精巧度)重要?换句话说,以下任一处理器在任何CPU上的运行速度都更快吗?
x << 1;
和
x << 10;
而且请不要讨厌我这个问题。:)
我发誓,我不想优化任何事情,我只是出于好奇而问这个问题。我知道,在大多数硬件有位移(例如的组件的命令shl
,shr
),它是一个命令。但这有多少关系(纳秒级或CPU精巧度)重要?换句话说,以下任一处理器在任何CPU上的运行速度都更快吗?
x << 1;
和
x << 10;
而且请不要讨厌我这个问题。:)
Answers:
可能取决于CPU。
但是,所有现代CPU(x86,ARM)都使用“桶式移位器”-一种专门设计用于在恒定时间内执行任意移位的硬件模块。
所以最重要的是……不。没有不同。
60000 mod register_size
。例如,一个32位处理器将只使用移位计数的5个最低有效位。
一些嵌入式处理器仅具有“移位1”指令。在此类处理器上,编译器将x << 3
变为((x << 1) << 1) << 1
。
我认为摩托罗拉MC68HCxx是受此限制的最受欢迎的系列之一。幸运的是,这种架构现在很少见,大多数现在都包括具有可变移位大小的桶形移位器。
具有许多现代派生功能的英特尔8051也无法移位任意位数。
有很多情况。
许多高速MPU具有桶形移位器,类似多路复用器的电子电路,它们可以在恒定时间内进行任何移位。
如果MPU仅具有1个移位x << 10
,通常会比较慢,因为通常是10个移位或2个字节的字节复制。
但是有一个常见的情况,x << 10
甚至比更快x << 1
。如果x是16位,则只关心其中的低6位(其他所有将被移出),因此MPU只需要加载低位字节,从而仅对8位存储器进行单个访问周期,而x << 10
需要两个访问周期。如果访问周期比移位慢(并清除低字节),x << 10
则会更快。这可能适用于具有快速板载程序ROM的微控制器,同时访问速度较慢的外部数据RAM。
除第3种情况外,编译器可能会关心有效位的数量,x << 10
并将进一步的操作优化为较小宽度的操作,例如将16x16乘法替换为16x8 1(因为低字节始终为零)。
注意,有些微控制器根本没有左移指令,add x,x
而是使用了。
在ARM上,这可以作为另一条指令的副作用来完成。因此,它们中的任何一个都完全没有延迟。
ADD R0, R1, R2 ASL #3
将R1和R2左移3位。
这取决于CPU和编译器。即使基础CPU带有桶形移位器的任意位移,也只有在编译器利用该资源的情况下才会发生。
请记住,在C和C ++中,将任何超出数据位宽度的内容移位都是“未定义的行为”。签名数据的右移也是“实现定义的”。不必太担心速度,而要担心在不同的实现上会得到相同的答案。
引用ANSI C第3.3.7节:
3.3.7按位移位运算符
句法
shift-expression: additive-expression shift-expression << additive-expression shift-expression >> additive-expression
约束条件
每个操作数应具有整数类型。
语义学
积分提升对每个操作数执行。结果的类型是提升后的左操作数的类型。如果右操作数的值为负或大于或等于提升后的左操作数的位宽度,则行为不确定。
E1 << E2的结果是E1左移E2位的位置;空位用零填充。如果E1具有无符号类型,则将结果的值乘以E1乘以2,再乘以幂E2,如果E1具有无符号长类型,则将结果取ULONG_MAX + 1为模,否则为UINT_MAX + 1。(常量ULONG_MAX和UINT_MAX在标头中定义。)
E1 >> E2的结果是E1右移E2位的位置。如果E1具有无符号类型,或者E1具有带符号类型和非负值,则结果的值是E1的商的整数部分除以数量2的幂次幂。如果E1具有带符号的类型和负值,则结果值是实现定义的。
所以:
x = y << z;
“ <<”:y×2 z(如果发生溢出则不确定);
x = y >> z;
“ >>”:为符号定义的实现方式定义(通常是算术移位的结果:y / 2 z)。
1u << 100
不是UB。这仅仅是0
1u << 100
一点点的移位可能是溢出;1u << 100
因为算术移位为0。在ANSI C下,<<
是一个移位。zh.wikipedia.org/wiki/Arithmetic_shift
x << (y & 31)
如果编译器知道目标体系结构的移位指令掩盖了计数,则仍然可以编译为没有AND指令的单个移位指令(就像x86一样)。(最好不要对掩码进行硬编码;从掩码中获取CHAR_BIT * sizeof(x) - 1
或从掩码中获取。)这对于编写旋转惯用法时非常有用,无论输入如何,该惯用法都可以编译成一条指令而无需任何C UB。(stackoverflow.com/questions/776508/…)。
在几代Intel CPU(P2或P3?不是AMD,如果我没记错的话)上,移位操作的速度简直太慢了。尽管按位移位1位应该总是很快的,因为它只能使用加法。要考虑的另一个问题是,固定位数的移位是否比可变长度的移位快。即使操作码的速度相同,在x86上,移位的非恒定右手操作数也必须占用CL寄存器,这对寄存器分配施加了额外的约束,并且也可能以这种方式降低程序速度。
shlx
/ shrx
/ sarx
(Haswell的后来和Ryzen)。CISC语义(如果count = 0,则标志未修改)在这里伤害了x86。 shl r32, cl
在Sandybridge系列中为3 oups(尽管Intel声称如果未使用标志结果,它可以取消其中一个uops)。AMD具有单码率shl r32, cl
(但对于扩展精度而言,则是慢速双移位shld r32, r32, cl
)
shl r32, cl
或立即数不是1的标志结果会使前端停滞,直到班次退休!(stackoverflow.com/questions/36510095/…)。编译器知道这一点,并使用单独的test
指令而不是使用移位的标志结果。(但是这种废物上的CPU指令,其中这不是一个问题,请参阅stackoverflow.com/questions/40354978/...)
与往常一样,它取决于周围的代码上下文:例如,您是否将其x<<1
用作数组索引?还是将其添加到其他内容中?在任一种情况下,小移位计数(1或2)可以经常优化甚至超过如果编译器结束有到刚刚移位。更不用说整个吞吐量与延迟,前端瓶颈之间的折衷。微小片段的性能不是一维的。
硬件移位指令不是编译器唯一的编译选项x<<1
,但其他答案大多是假设的。
x << 1
完全等于x+x
unsigned和2的补码有符号整数。编译器在编译时始终知道目标对象是什么硬件,因此他们可以利用这样的技巧。
在Intel Haswell上,add
每个时钟吞吐量为4,但是shl
立即计数每个时钟吞吐量仅为2。(有关说明表和其他链接,请参见http://agner.org/optimize/。x86标签Wiki)。SIMD向量移位为每个时钟1个(在Skylake中为2),但SIMD向量整数相加为每个时钟2个(在Skylake中为3)。但是,延迟是相同的:1个周期。
还有一种特殊的移一编码,shl
可在操作码中隐含计数。8086没有立即计数移位,只有一次和按cl
寄存器。这与右移最相关,因为除非对内存操作数进行移位,否则您只需为左移添加即可。但是,如果以后需要该值,最好先加载到寄存器中。但无论如何,shl eax,1
还是add eax,eax
比短1个字节shl eax,10
,并且代码大小会直接(解码/前端瓶颈)或间接(L1I代码缓存未命中)影响性能。
通常,在x86上的寻址模式下,有时可以将小的移位计数优化为缩放索引。如今,大多数其他常用的体系结构都是RISC,并且没有缩放索引寻址模式,但是x86足够常见,值得一提。(例如,如果您要索引4字节元素的数组,则可以将的比例因子增加1 int arr[]; arr[x<<1]
)。
在x
仍然需要原始值的情况下,通常需要复制+移位。但是大多数x86整数指令都是就地操作。 (目标是诸如add
或的指令的来源之一shl
。)x86-64 System V调用约定在寄存器中传递args,第一个arg进入edi
并且返回值在eax
,因此返回的函数x<<10
还使编译器发出copy + shift码。
该LEA
指令允许您进行移位和相加(移位计数为0到3,因为它使用寻址模式的机器编码)。它将结果放在单独的寄存器中。
gcc和clang都以相同的方式优化了这些功能,就像您在Godbolt编译器资源管理器中看到的那样:
int shl1(int x) { return x<<1; }
lea eax, [rdi+rdi] # 1 cycle latency, 1 uop
ret
int shl2(int x) { return x<<2; }
lea eax, [4*rdi] # longer encoding: needs a disp32 of 0 because there's no base register, only scaled-index.
ret
int times5(int x) { return x * 5; }
lea eax, [rdi + 4*rdi]
ret
int shl10(int x) { return x<<10; }
mov eax, edi # 1 uop, 0 or 1 cycle latency
shl eax, 10 # 1 uop, 1 cycle latency
ret
具有2个组件的LEA在最近的Intel和AMD CPU上具有1个周期的延迟和2个时钟的吞吐量。(桑迪布里奇(Sandybridge)家庭和推土机/里森(Ryzen)。在Intel上,每时钟吞吐量只有1个,延迟为3c lea eax, [rdi + rsi + 123]
。(相关:为什么此C ++代码比我用来测试Collatz猜想的手写程序集还要快?在这方面进行了详细介绍。)
无论如何,复制+移位10需要单独的mov
指令。在许多最近的CPU上,它的延迟可能为零,但仍占用前端带宽和代码大小。(x86的MOV真的可以“免费”吗?为什么我根本不能复制它?)
还相关:如何在x86中仅使用2条连续的leal指令将寄存器乘以37?。
编译器还可以自由地转换周围的代码,因此无需进行实际移位,也可以将其与其他操作结合使用。
例如,if(x<<1) { }
可以使用and
来检查除高位以外的所有位。在x86上,你会使用一个test
指令一样,test eax, 0x7fffffff
/jz .false
来代替shl eax,1 / jz
。此优化适用于任何班次计数,也适用于大班次缓慢(例如Pentium 4)或不存在(某些微控制器)的机器。
许多ISA除了移位之外还具有位操作指令。例如PowerPC有很多位域提取/插入指令。或者ARM将源操作数的移位作为任何其他指令的一部分。(因此move
,使用移位源,移位/旋转指令只是的一种特殊形式。)
记住,C不是汇编语言。在调整源代码以有效地进行编译时,请始终查看优化的编译器输出。