如果 CPU 是一个二进制机器,为什么它在位操作上很慢?
If CPU is a binary machine, why is it slow on bit manipulations?
我发现与其二进制/双态性质相反,x86 CPUs 在处理二进制操作指令(例如 SHR、BT、BTR、ROL 和类似指令)时非常慢。
例如,我从某个地方读到,位移/旋转超过 1 个位置被认为是缓慢的(具有高延迟、性能损失和那些可怕的东西)。当操作数在内存中时更糟(内存双态外围设备不也是吗?)
shl eax,1 ;ok
shl eax,7 ;slow?
那么是什么让它们变慢了?具有讽刺意味的是,像 CPU 这样的二进制机器在位操作上很慢,而这种操作本应是自然的。它给人的印象是二进制 CPU 很难将位移位到位!
编辑:现在在再次查看手册中的 SHL 条目后,它确实涉及一些繁重的微代码逻辑!
从Intel's vol.2 manual for shl
...
Operation
TemporaryCount = Count & 0x1F;
TemporaryDestination = Destination;
while(TemporaryCount != 0) {
if(Instruction == SAL || Instruction == SHL) {
CF = MSB(Destination);
Destination = Destination << 1;
}
//instruction is SAR or SHR
else {
CF = LSB(Destination);
if(Instruction == SAR) Destination = Destination / 2; //Signed divide, rounding toward negative infinity
//Instruction is SHR
else Destination = Destination / 2; //Unsigned divide
}
TemporaryCount = TemporaryCount - 1;
}
//Determine overflow
if(Count & 0x1F == 1) {
if(Instruction == SAL || Instruction == SHL) OF = MSB(Destination) ^ CF;
else if(Instruction == SAR) OF = 0;
//Instruction == SHR
else OF = MSB(TemporaryDestination);
}
else OF = Undefined;
难以置信地看到如此简单的布尔代数变成了实现的噩梦。
这只是指令的伪代码,具体说明了指令的作用。该指令实际上并不是这样实现的。实际上,所有现代 CPU 都有桶形移位器或类似的东西,允许它们在单个周期内移动任意量。例如,请参阅 Agner Fog's tables,其中显示几乎所有位摆弄指令的延迟为 1。
一些位摆弄指令比较慢,这里有一些例子:
由于 (a) 读-修改-写操作和 ( b) 他们做的位串索引
rcr
旋转量大于 1
的速度很慢,因为几乎从不需要这条指令,因此没有优化
pdep
和 pext
在 Intel 上稍微慢一些,在 AMD 上慢得多,可能是因为它们的实现非常复杂,将实现分开会更容易。
在旧处理器(例如 8086)上,CPU 将花费与移位量一样多的周期,每个周期进行一次移位。这种实现方式允许 ALU 用于移位而无需任何额外的硬件,从而减少了处理器所需的门数。我所知道的现代 CPU 没有这种性能行为。
请注意。
shl eax,1 ; opcode: d1 e0
shl eax,7 ; opcode: c1 e0 07
实际上是具有不同操作码的不同指令,可能由 ALU 的不同逻辑块处理。它们在汇编中使用相同的助记符,这可能会造成混淆,但从 CPU 的角度来看,它们是具有不同操作码和编码的不同指令。
我发现与其二进制/双态性质相反,x86 CPUs 在处理二进制操作指令(例如 SHR、BT、BTR、ROL 和类似指令)时非常慢。
例如,我从某个地方读到,位移/旋转超过 1 个位置被认为是缓慢的(具有高延迟、性能损失和那些可怕的东西)。当操作数在内存中时更糟(内存双态外围设备不也是吗?)
shl eax,1 ;ok
shl eax,7 ;slow?
那么是什么让它们变慢了?具有讽刺意味的是,像 CPU 这样的二进制机器在位操作上很慢,而这种操作本应是自然的。它给人的印象是二进制 CPU 很难将位移位到位!
编辑:现在在再次查看手册中的 SHL 条目后,它确实涉及一些繁重的微代码逻辑!
从Intel's vol.2 manual for shl
...
Operation
TemporaryCount = Count & 0x1F;
TemporaryDestination = Destination;
while(TemporaryCount != 0) {
if(Instruction == SAL || Instruction == SHL) {
CF = MSB(Destination);
Destination = Destination << 1;
}
//instruction is SAR or SHR
else {
CF = LSB(Destination);
if(Instruction == SAR) Destination = Destination / 2; //Signed divide, rounding toward negative infinity
//Instruction is SHR
else Destination = Destination / 2; //Unsigned divide
}
TemporaryCount = TemporaryCount - 1;
}
//Determine overflow
if(Count & 0x1F == 1) {
if(Instruction == SAL || Instruction == SHL) OF = MSB(Destination) ^ CF;
else if(Instruction == SAR) OF = 0;
//Instruction == SHR
else OF = MSB(TemporaryDestination);
}
else OF = Undefined;
难以置信地看到如此简单的布尔代数变成了实现的噩梦。
这只是指令的伪代码,具体说明了指令的作用。该指令实际上并不是这样实现的。实际上,所有现代 CPU 都有桶形移位器或类似的东西,允许它们在单个周期内移动任意量。例如,请参阅 Agner Fog's tables,其中显示几乎所有位摆弄指令的延迟为 1。
一些位摆弄指令比较慢,这里有一些例子:
-
由于 (a) 读-修改-写操作和 ( b) 他们做的位串索引
rcr
旋转量大于1
的速度很慢,因为几乎从不需要这条指令,因此没有优化pdep
和pext
在 Intel 上稍微慢一些,在 AMD 上慢得多,可能是因为它们的实现非常复杂,将实现分开会更容易。
在旧处理器(例如 8086)上,CPU 将花费与移位量一样多的周期,每个周期进行一次移位。这种实现方式允许 ALU 用于移位而无需任何额外的硬件,从而减少了处理器所需的门数。我所知道的现代 CPU 没有这种性能行为。
请注意。
shl eax,1 ; opcode: d1 e0
shl eax,7 ; opcode: c1 e0 07
实际上是具有不同操作码的不同指令,可能由 ALU 的不同逻辑块处理。它们在汇编中使用相同的助记符,这可能会造成混淆,但从 CPU 的角度来看,它们是具有不同操作码和编码的不同指令。