保存左移 (SHL) 中移出的位
Saving the bits that are shifted off in a left-shift (SHL)
考虑以下 Intel 8086 汇编程序:
CX 持有非零值。
L: ADD AX, AX
ADC DX, 0
LOOP L
我被要求理解上面的代码并重写它以提高效率。
据我了解:
- 将2^CX * AX的值存入AX
- 统计进程中进位标志被置1的次数,并保存在DX中。
假设这是正确的,我认为更好的代码应该是 AX、CX 倍的 SHL 值。
SHL AX, CX
但是,我想不出一种方法来计算过程中的进位位。 (或计算原始 AX 的 CX 最高有效位中“1”位的数量。)
非常感谢任何指导和帮助。
根据您的非零 cl 假设,此指令对将是等效的:
shld dx,ax,cl
shl ax,cl
如果您没有 shld
(例如,因为您的代码必须 运行 在 80286 上,或者如果您想在非 x86 CPU 上使用相同的代码)您可以执行以下伪代码:
if CX < 16
DX = AX SHR (16 - CX)
AX = AX SHL 16
else
if CX < 32
DX = AX SHL (CX - 16)
else
DX = 0
endif
AX = 0
endif
在其他答案中发布的将移位后的 AX 的高位放入 DX 的任何变体之后,您需要对 DX 进行 popcnt(计算 1 位的数量)的一些变体。执行此操作的代码有些冗长,下面显示了 32 位模式的示例。对于真正的 8086,最好使用 256 字节 table 将索引转换为索引中 1 的位数,序列如下:
xor bx,bx
mov bl,dl ;bl = lower bits
mov dl,table[bx] ;dl = lower # bits set
mov bl,dh ;bl = upper bits
add dl,table[bx] ;dl = total # bits set
; ...
xor dh,dh ;optional, clear upper bits dx
edi 示例代码的 32 位 popcnt:
mov edx,edi ;edx = edi
shr edx,1 ;mov upr 2 bit field bits to lwr
and edx,055555555h ; and mask them
sub edi,edx ;edi = 2 bit field counts
; 0->0, 1->1, 2->1, 3->1
mov eax,edi
shr edi,02h ;mov upr 2 bit field counts to lwr
and eax,033333333h ;eax = lwr 2 bit field counts
and edi,033333333h ;edx = upr 2 bit field counts
add edi,eax ;edi = 4 bit field counts
mov eax,edi
shr eax,04h ;mov upr 4 bit field counts to lwr
add eax,edi ;eax = 8 bit field counts
and eax,00f0f0f0fh ; after the and
imul eax,eax,01010101h ;eax bit 24->28 = bit count
shr eax,018h ;eax bit 0->4 = bit count
您对当前代码工作原理的理解基本上是正确的。为了确保我们理解它,让我们逐步执行示例。通常情况下,手头有一个调试器就可以完成这种事情,但我们真正需要的只是我们的头脑和一个可以显示二进制值的计算器。
假设 AX
是 55312(选择较大的初始值可以让我们立即看到进位的效果)。 CX
会是4,当然DX
是预置零的。
- 迭代 1:
55312 + 55312
溢出 16 位值的范围,因此设置了进位位,AX
现在是 45088。因为设置了进位,DX
= 1. CX
递减为 3.
- 迭代2:
45088 + 45088
再次溢出,所以进位位被设置,AX
现在是24640。
DX
= 2; CX
= 2.
- 迭代3:
24640 + 24640
没有溢出,所以进位位没有设置,AX
现在是49280。
DX
= 2; CX
= 1.
- 迭代 4:
49280 + 4928
溢出,所以进位位被置位,AX
现在是 33024。
DX
= 3; CX
= 0.
因此,当我们完成时,DX
是 3。如果我们查看起始值的二进制表示:
1101 1000 0001 0000
↑ ↑
bit 15 bit 0
可以看出你的直觉得到了证实:这个值的高4(CX
)位中set(1)位的个数是3,等于DX
。
这些类型的位级观察会导致巧妙的位技巧,是大多数优化突破的关键,您已经通过思考您的代码实际做了什么来发现这一点,非常好
收拾思路,让我们把算法显式写出来,假设AX
是输入值,CX
包含迭代次数:
- 隔离
AX
中的高 CX
位,丢弃其余位。
- 计算
AX
中设置的位数。
如果我们的目标是现代处理器——Intel Nehalem、AMD Barcelona 和更新的处理器——使用 SHR
右移然后使用 POPCNT
指令将是一个简单的问题计算所需范围内的设置位数。例如:
; AX == input value
; CX == number of iterations
neg cx
add cx, 16 ; cx = 16 - cx
shr ax, cl ; ax = ax << cx
popcnt ax, ax ; ax = # of 1 bits in ax
这将是快速。没有branching/looping;只需 4 个简单的说明。您只查看了执行时间中的少数几个周期,不可能发生分支预测错误。
但是,如果您的目标是较旧的 CPU,而 POPCNT
指令不存在怎么办?好吧,你需要模仿它。有多种快速方法可以实现 population count/Hamming 权重算法。在 Pentium 或更高版本上,最快的方法如下:
; AX == input value
; CX == number of iterations
neg cx
add cx, 16 ; cx = 16 - cx
shr ax, cl ; ax = ax << cx
; emulate popcnt
mov dx, ax
shr dx, 1
and dx, 21845
sub ax, dx
mov cx, ax
and ax, 13107
shr cx, 2
and cx, 13107
add cx, ax
mov dx, cx
shr dx, 4
add dx, cx
and dx, 3855
mov ax, dx
shr ax, 8
add ax, dx
and ax, 63
这是 this method 的 16 位改编,它使用一系列移位、加法和掩码并行化位计数。这些都是简单的指令,而且它仍然是无分支的,所以它在大多数处理器上都非常快……但不是 8088/8086!在那些古老的恐龙身上,即使是像这样的简单指令也需要多个周期来执行,更糟糕的是,它们都必须 解码 ,因此缓慢的内存访问速度往往会减慢速度。如果你真的想针对 8088/8086 优化它,你需要使用查找 table 来实现人口计数算法。而且,在这些处理器上,经常被遗忘的 1 字节 XLAT
指令是迄今为止在 table:
中查找值的最快方法
LUT DB 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8
; AX == input value
; CX == number of iterations
neg cx
add cx, 16 ; cx = 16 - cx
shr ax, cl ; ax = ax << cx
; emulate popcnt using LUT
mov bx, OFFSET LUT
xlat ; equivalent to: mov al, [bx + al]
xchg al, ah
xlat ; equivalent to: mov al, [bx + al]
add al, ah
xor ah, ah
这需要 256 个字节来存储代码中的查找 table (LUT),但在 8088/ 上 绝对 的执行速度更快8086 比做所有这些算术。我们可以通过计算周期来大致估计执行速度:
neg cx ; 3 cycles
add cx, 16 ; 4 cycles
shr ax, cl ; 8+(4*CL) cycles
mov bx, OFFSET LUT ; 4 cycles
xlat ; 11 cycles
xchg al, ah ; 4 cycles
xlat ; 11 cycles
add al, ah ; 3 cycles
xor ah, ah ; 3 cycles
;-----------------
; 51+(4*CL) cycles
注意这里慢的指令是右移。它需要固定的 8 个周期,加上每个移位的 4 个额外周期( 即 ,移位计数,在 CL
中)。不幸的是,我们对此无能为力。这意味着我们有大约 50 个周期的最佳情况性能,最差情况下的性能仍低于 120 个周期。
将其与您的原始代码进行比较:
xor dx, dx ; 3 cycles
L: add ax, ax ; 3 cycles
adc dx, 0 ; 4 cycles
loop L ; taken: 17 cycles; not-taken: 5 cycles
;---------------------------------------
; 8+(24*CL) cycles
这里,大约循环数取决于 CX
(循环计数),因为它决定了分支被采用的次数。所以,在最好的情况下,这段代码大约需要 32 个周期;在最坏的情况下,大约需要 400 个周期。
我想重申,循环计数并不准确,即使在像 8086 这样的简单芯片上也是如此,但它确实为我们提供了一种合理的性能估算方法。您的原始代码确实具有稍微更好的 best-case 性能(在 CX
很小的情况下),但是我们优化的、基于位计数的 LUT 方法有很多最坏情况 性能更好,更重要的是,它扩展性更好。您可以在以下两种方法的图形比较中清楚地看到这一点:
只要CX
小,你原来的代码就是合理的实现。但是随着 CX
变大,由于所有这些 LOOP
,例程变得 呈指数级 越来越慢。我们基于 LUT 的方法具有更大的 开销 (这甚至还没有计算 LUT 添加到二进制文件中的膨胀),但随着 CX
变得更大,它真正开始得到回报. 总而言之,我们用增加的代码大小换取了执行速度,这是一种常见的优化权衡。
现在,我需要坦白。我一直在暗中假设 CX
永远不会大于 16。如果 CX
大于 16,我向您展示的所有 "optimized" 代码都赢了' 工作,因为 SHR
指令将尝试移出太多位。如果您需要处理 CX
> 16,那么您需要调整代码,使其 clamps CX
小于或等于16. 这意味着要么是一个条件分支,要么是一系列巧妙的位旋转指令,其中任何一个都会增加代码的复杂性并增加其循环计数。换句话说,这会增加 "optimized" 方法的 baseline overhead,但这种方法将继续 scale 比你原来的更好方法。从图形上看,红线将向上平移。
(您的原始代码不需要任何修改——它可以处理 CX
高达 65,535 的值而不会受到任何额外的惩罚,因为它只是保持 LOOP
ing。但正如我们已经看到的,每个 LOOP
s 都会付出显着的性能损失。)
"tweaked" 代码看起来像这样:
LUT DB 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8
; AX == input value
; CX == number of iterations
mov bx, 16 ; 4 cycles
cmp cx, bx ; cx < 16? ; 3 cycles
jae SkipShift ; 4 cycles (fall-through); 16 cycles (branch)
sub bx, cx ; 3 cycles
mov cx, bx ; cx -= 16 ; 2 cycles
shr ax, cl ; ax <<= cx ; 8+(4*CL) cycles
SkipShift:
mov bx, OFFSET LUT ; 4 cycles
xlat ; 11 cycles
xchg al, ah ; 4 cycles
xlat ; 11 cycles
add al, ah ; 3 cycles
xor ah, ah ; 3 cycles
如果采用此 JAE
,您将支付 16 个周期的罚款,但在这种情况下我们可以跳过减法和移位,从而弥补那些丢失的周期。如果 JAE
没有被采用,并且执行失败,我们只损失了 4 个周期。总体而言,最佳情况下的性能约为 60 个周期,而最坏情况下的性能大约是其两倍。
考虑以下 Intel 8086 汇编程序:
CX 持有非零值。
L: ADD AX, AX
ADC DX, 0
LOOP L
我被要求理解上面的代码并重写它以提高效率。 据我了解:
- 将2^CX * AX的值存入AX
- 统计进程中进位标志被置1的次数,并保存在DX中。
假设这是正确的,我认为更好的代码应该是 AX、CX 倍的 SHL 值。
SHL AX, CX
但是,我想不出一种方法来计算过程中的进位位。 (或计算原始 AX 的 CX 最高有效位中“1”位的数量。)
非常感谢任何指导和帮助。
根据您的非零 cl 假设,此指令对将是等效的:
shld dx,ax,cl
shl ax,cl
如果您没有 shld
(例如,因为您的代码必须 运行 在 80286 上,或者如果您想在非 x86 CPU 上使用相同的代码)您可以执行以下伪代码:
if CX < 16
DX = AX SHR (16 - CX)
AX = AX SHL 16
else
if CX < 32
DX = AX SHL (CX - 16)
else
DX = 0
endif
AX = 0
endif
在其他答案中发布的将移位后的 AX 的高位放入 DX 的任何变体之后,您需要对 DX 进行 popcnt(计算 1 位的数量)的一些变体。执行此操作的代码有些冗长,下面显示了 32 位模式的示例。对于真正的 8086,最好使用 256 字节 table 将索引转换为索引中 1 的位数,序列如下:
xor bx,bx
mov bl,dl ;bl = lower bits
mov dl,table[bx] ;dl = lower # bits set
mov bl,dh ;bl = upper bits
add dl,table[bx] ;dl = total # bits set
; ...
xor dh,dh ;optional, clear upper bits dx
edi 示例代码的 32 位 popcnt:
mov edx,edi ;edx = edi
shr edx,1 ;mov upr 2 bit field bits to lwr
and edx,055555555h ; and mask them
sub edi,edx ;edi = 2 bit field counts
; 0->0, 1->1, 2->1, 3->1
mov eax,edi
shr edi,02h ;mov upr 2 bit field counts to lwr
and eax,033333333h ;eax = lwr 2 bit field counts
and edi,033333333h ;edx = upr 2 bit field counts
add edi,eax ;edi = 4 bit field counts
mov eax,edi
shr eax,04h ;mov upr 4 bit field counts to lwr
add eax,edi ;eax = 8 bit field counts
and eax,00f0f0f0fh ; after the and
imul eax,eax,01010101h ;eax bit 24->28 = bit count
shr eax,018h ;eax bit 0->4 = bit count
您对当前代码工作原理的理解基本上是正确的。为了确保我们理解它,让我们逐步执行示例。通常情况下,手头有一个调试器就可以完成这种事情,但我们真正需要的只是我们的头脑和一个可以显示二进制值的计算器。
假设 AX
是 55312(选择较大的初始值可以让我们立即看到进位的效果)。 CX
会是4,当然DX
是预置零的。
- 迭代 1:
55312 + 55312
溢出 16 位值的范围,因此设置了进位位,AX
现在是 45088。因为设置了进位,DX
= 1.CX
递减为 3. - 迭代2:
45088 + 45088
再次溢出,所以进位位被设置,AX
现在是24640。
DX
= 2;CX
= 2. - 迭代3:
24640 + 24640
没有溢出,所以进位位没有设置,AX
现在是49280。
DX
= 2;CX
= 1. - 迭代 4:
49280 + 4928
溢出,所以进位位被置位,AX
现在是 33024。
DX
= 3;CX
= 0.
因此,当我们完成时,DX
是 3。如果我们查看起始值的二进制表示:
1101 1000 0001 0000
↑ ↑
bit 15 bit 0
可以看出你的直觉得到了证实:这个值的高4(CX
)位中set(1)位的个数是3,等于DX
。
这些类型的位级观察会导致巧妙的位技巧,是大多数优化突破的关键,您已经通过思考您的代码实际做了什么来发现这一点,非常好
收拾思路,让我们把算法显式写出来,假设AX
是输入值,CX
包含迭代次数:
- 隔离
AX
中的高CX
位,丢弃其余位。 - 计算
AX
中设置的位数。
如果我们的目标是现代处理器——Intel Nehalem、AMD Barcelona 和更新的处理器——使用 SHR
右移然后使用 POPCNT
指令将是一个简单的问题计算所需范围内的设置位数。例如:
; AX == input value
; CX == number of iterations
neg cx
add cx, 16 ; cx = 16 - cx
shr ax, cl ; ax = ax << cx
popcnt ax, ax ; ax = # of 1 bits in ax
这将是快速。没有branching/looping;只需 4 个简单的说明。您只查看了执行时间中的少数几个周期,不可能发生分支预测错误。
但是,如果您的目标是较旧的 CPU,而 POPCNT
指令不存在怎么办?好吧,你需要模仿它。有多种快速方法可以实现 population count/Hamming 权重算法。在 Pentium 或更高版本上,最快的方法如下:
; AX == input value
; CX == number of iterations
neg cx
add cx, 16 ; cx = 16 - cx
shr ax, cl ; ax = ax << cx
; emulate popcnt
mov dx, ax
shr dx, 1
and dx, 21845
sub ax, dx
mov cx, ax
and ax, 13107
shr cx, 2
and cx, 13107
add cx, ax
mov dx, cx
shr dx, 4
add dx, cx
and dx, 3855
mov ax, dx
shr ax, 8
add ax, dx
and ax, 63
这是 this method 的 16 位改编,它使用一系列移位、加法和掩码并行化位计数。这些都是简单的指令,而且它仍然是无分支的,所以它在大多数处理器上都非常快……但不是 8088/8086!在那些古老的恐龙身上,即使是像这样的简单指令也需要多个周期来执行,更糟糕的是,它们都必须 解码 ,因此缓慢的内存访问速度往往会减慢速度。如果你真的想针对 8088/8086 优化它,你需要使用查找 table 来实现人口计数算法。而且,在这些处理器上,经常被遗忘的 1 字节 XLAT
指令是迄今为止在 table:
LUT DB 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8
; AX == input value
; CX == number of iterations
neg cx
add cx, 16 ; cx = 16 - cx
shr ax, cl ; ax = ax << cx
; emulate popcnt using LUT
mov bx, OFFSET LUT
xlat ; equivalent to: mov al, [bx + al]
xchg al, ah
xlat ; equivalent to: mov al, [bx + al]
add al, ah
xor ah, ah
这需要 256 个字节来存储代码中的查找 table (LUT),但在 8088/ 上 绝对 的执行速度更快8086 比做所有这些算术。我们可以通过计算周期来大致估计执行速度:
neg cx ; 3 cycles
add cx, 16 ; 4 cycles
shr ax, cl ; 8+(4*CL) cycles
mov bx, OFFSET LUT ; 4 cycles
xlat ; 11 cycles
xchg al, ah ; 4 cycles
xlat ; 11 cycles
add al, ah ; 3 cycles
xor ah, ah ; 3 cycles
;-----------------
; 51+(4*CL) cycles
注意这里慢的指令是右移。它需要固定的 8 个周期,加上每个移位的 4 个额外周期( 即 ,移位计数,在 CL
中)。不幸的是,我们对此无能为力。这意味着我们有大约 50 个周期的最佳情况性能,最差情况下的性能仍低于 120 个周期。
将其与您的原始代码进行比较:
xor dx, dx ; 3 cycles
L: add ax, ax ; 3 cycles
adc dx, 0 ; 4 cycles
loop L ; taken: 17 cycles; not-taken: 5 cycles
;---------------------------------------
; 8+(24*CL) cycles
这里,大约循环数取决于 CX
(循环计数),因为它决定了分支被采用的次数。所以,在最好的情况下,这段代码大约需要 32 个周期;在最坏的情况下,大约需要 400 个周期。
我想重申,循环计数并不准确,即使在像 8086 这样的简单芯片上也是如此,但它确实为我们提供了一种合理的性能估算方法。您的原始代码确实具有稍微更好的 best-case 性能(在 CX
很小的情况下),但是我们优化的、基于位计数的 LUT 方法有很多最坏情况 性能更好,更重要的是,它扩展性更好。您可以在以下两种方法的图形比较中清楚地看到这一点:
只要CX
小,你原来的代码就是合理的实现。但是随着 CX
变大,由于所有这些 LOOP
,例程变得 呈指数级 越来越慢。我们基于 LUT 的方法具有更大的 开销 (这甚至还没有计算 LUT 添加到二进制文件中的膨胀),但随着 CX
变得更大,它真正开始得到回报. 总而言之,我们用增加的代码大小换取了执行速度,这是一种常见的优化权衡。
现在,我需要坦白。我一直在暗中假设 CX
永远不会大于 16。如果 CX
大于 16,我向您展示的所有 "optimized" 代码都赢了' 工作,因为 SHR
指令将尝试移出太多位。如果您需要处理 CX
> 16,那么您需要调整代码,使其 clamps CX
小于或等于16. 这意味着要么是一个条件分支,要么是一系列巧妙的位旋转指令,其中任何一个都会增加代码的复杂性并增加其循环计数。换句话说,这会增加 "optimized" 方法的 baseline overhead,但这种方法将继续 scale 比你原来的更好方法。从图形上看,红线将向上平移。
(您的原始代码不需要任何修改——它可以处理 CX
高达 65,535 的值而不会受到任何额外的惩罚,因为它只是保持 LOOP
ing。但正如我们已经看到的,每个 LOOP
s 都会付出显着的性能损失。)
"tweaked" 代码看起来像这样:
LUT DB 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8
; AX == input value
; CX == number of iterations
mov bx, 16 ; 4 cycles
cmp cx, bx ; cx < 16? ; 3 cycles
jae SkipShift ; 4 cycles (fall-through); 16 cycles (branch)
sub bx, cx ; 3 cycles
mov cx, bx ; cx -= 16 ; 2 cycles
shr ax, cl ; ax <<= cx ; 8+(4*CL) cycles
SkipShift:
mov bx, OFFSET LUT ; 4 cycles
xlat ; 11 cycles
xchg al, ah ; 4 cycles
xlat ; 11 cycles
add al, ah ; 3 cycles
xor ah, ah ; 3 cycles
如果采用此 JAE
,您将支付 16 个周期的罚款,但在这种情况下我们可以跳过减法和移位,从而弥补那些丢失的周期。如果 JAE
没有被采用,并且执行失败,我们只损失了 4 个周期。总体而言,最佳情况下的性能约为 60 个周期,而最坏情况下的性能大约是其两倍。