我绝对不想优化任何东西,我只是出于好奇想问这个问题。我知道在大多数硬件上都有一个位移的汇编指令(例如 shl
, shr
),这是一个单独的指令。但是,无论你移动多少位,它是否会影响性能(以纳秒或 CPU 指令为单位)?换句话说,在任何 CPU 上,以下哪种方法更快?
x << 1;
并且
x << 10;
请不要因为这个问题而讨厌我。 :)
我绝对不想优化任何东西,我只是出于好奇想问这个问题。我知道在大多数硬件上都有一个位移的汇编指令(例如 shl
, shr
),这是一个单独的指令。但是,无论你移动多少位,它是否会影响性能(以纳秒或 CPU 指令为单位)?换句话说,在任何 CPU 上,以下哪种方法更快?
x << 1;
并且
x << 10;
请不要因为这个问题而讨厌我。 :)
可能与CPU有关。
然而,所有现代的CPU(x86,ARM)都使用“移位器”--一种专门设计用于在恒定时间内执行任意移位操作的硬件模块。
因此,最终结论是...没有区别。
shl
的性能仍然与计数无关,只是比较慢(4个周期延迟,或者在Prescott上为1c:http://agner.org/optimize/)。但是聪明的编译器会使用“add x,x”进行左移位1(在Prescott之前的双泵ALU上具有0.5c延迟)。右移位1没有这样的快捷方式。 - Peter Cordes一些嵌入式处理器只有“移位1位”的指令。在这种处理器上,编译器会将x << 3
转换为((x << 1) << 1) << 1
。
我认为Motorola 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乘法(因为低字节始终为零)。
请注意,有些微控制器根本没有左移指令,它们使用add x,x
代替。
在ARM上,这可以作为另一条指令的副作用而完成。因此,在任何一种情况下都没有潜在的延迟。
ADD R0,R1,R2 ASL #3
将使 R1 和 R2 左移 3 位。 - Vovanium可以想象,在8位处理器上,对于16位值,x<<1
实际上比x<<10
慢得多。
例如,x<<1
的合理翻译可能是:
byte1 = (byte1 << 1) | (byte2 >> 7)
byte2 = (byte2 << 1)
相比之下,x<<10
更为简单:
byte1 = (byte2 << 2)
byte2 = 0
x<<1
的移位次数比x<<10
更多,甚至更远。此外,x<<10
的结果不取决于byte1的内容。这可以进一步加速操作。所以:3.3.7 Bitwise shift operators
Syntax
shift-expression: additive-expression shift-expression << additive-expression shift-expression >> additive-expression
Constraints
Each of the operands shall have integral type.
Semantics
The integral promotions are performed on each of the operands. The type of the result is that of the promoted left operand. If the value of the right operand is negative or is greater than or equal to the width in bits of the promoted left operand, the behavior is undefined.
The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 multiplied by the quantity, 2 raised to the power E2, reduced modulo ULONG_MAX+1 if E1 has type unsigned long, UINT_MAX+1 otherwise. (The constants ULONG_MAX and UINT_MAX are defined in the header .)
The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 divided by the quantity, 2 raised to the power E2 . If E1 has a signed type and a negative value, the resulting value is implementation-defined.
x = y << z;
x = y >> z;
1u << 100
不是未定义行为。它只是0。 - Armen Tsirunyan1u << 100
作为位移操作可能会导致溢出;而作为算术移位则为0。在 ANSI C 中,<<
是位移操作。http://en.wikipedia.org/wiki/Arithmetic_shift - the wolfx << (y & 31)
仍然可以编译为单个移位指令而无需AND指令。(最好不要硬编码掩码;从CHAR_BIT * sizeof(x) - 1
或其他地方获取。)这对于编写旋转惯用语很有用,它可以编译为单个指令,而不管输入如何都不会出现C UB。(https://dev59.com/J3RA5IYBdhLWcg3w8SiJ)。 - Peter Cordes在一些Intel CPU的世代中(P2或P3?不过好像不包括AMD),位移操作非常慢。但是将数字左移1位应该总是很快,因为它只需使用加法。另一个需要考虑的问题是,按恒定数量的位移是否比按变长位移更快。即使操作码的速度相同,在x86上,位移的非恒定右操作数必须占用CL寄存器,这对寄存器分配施加了额外的限制,并可能以此方式减慢程序的运行速度。
shlx
/ shrx
/ sarx
(Haswell及更高版本和Ryzen)。CISC语义(如果count = 0,则标志未修改)在这里对x86造成了伤害。在Sandybridge系列上,“shl r32,cl”需要3个微操作(尽管英特尔声称如果不使用标志结果,则可以取消其中一个微操作)。AMD具有单微操作“shl r32,cl”(但对于扩展精度而言,双移位较慢,“shld r32,r32,cl”)。 - Peter Cordesshl r32, cl
的标志结果或使用除1以外的立即数会阻塞前端,直到移位操作完成!(https://dev59.com/y1oV5IYBdhLWcg3wPctv)。编译器知道这一点,并使用单独的`test`指令而不是使用移位操作的标志结果。(但在CPU不受影响的情况下,这会浪费指令,参见https://dev59.com/Huk6XIcBkEYKwwoYAfK1#40355466) - Peter Cordes一如既往,这取决于周围的代码上下文:例如,您将x<<1
用作数组索引吗?还是将其添加到其他内容中?在任何情况下,小的移位计数(1或2)通常可以比编译器最终必须进行的仅仅移位更加优化。更不用说整个吞吐量与延迟与前端瓶颈之间的权衡了。一个微小片段的性能不是单一维度的。
硬件移位指令不是编译器编译x<<1
的唯一选择,但其他答案大多都假定了这一点。
x << 1
等价于x+x
对于无符号整数和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
比shl eax,10
少一个字节,并且代码大小直接(解码/前端瓶颈)或间接(L1I代码缓存未命中)影响性能。更一般地说,在x86上,小的移位计数有时可以优化为缩放索引的寻址模式。现在大多数其他常用的体系结构都是RISC结构,没有缩放索引寻址模式,但x86是一个足够常见的架构,值得一提。(例如,如果您正在索引一个4字节元素的数组,则可以通过将比例因子增加1来增加空间int arr[]; arr[x<<1]
)。
在需要保留变量 x
原始值的情况下,需要进行复制和移位操作。但是大多数 x86 整数指令都是就地操作(目标寄存器是类似于add
或shl
这样的指令的源之一)。x86-64 System V 调用约定使用寄存器传递参数,第一个参数在 edi
中,返回值在 eax
中,因此返回 x<<10
的函数也会使编译器生成复制和移位代码。
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
lea eax,[rdi + rsi + 123]
,每时钟周期吞吐量为1,延迟为3个时钟周期。(相关: 为什么这段C++代码比我手写的汇编测试Collatz猜想更快?详细介绍了这一点。)mov
指令。在许多现代CPU上它可能是零延迟,但它仍然需要前端带宽和代码大小。( x86的MOV指令真的可以“免费”吗?为什么我根本无法重现这个结果?)