保存左移(SHL)中移位的位

时间:2017-06-19 18:40:05

标签: performance assembly x86 bit-shift x86-16

考虑以下Intel 8086汇编程序:

CX保持非零值。

L: ADD AX, AX
   ADC DX, 0
   LOOP L

我被要求了解上面的代码并重写它以提高效率。 据我所知:

  1. 它将2 ^ CX * AX的值保存到AX
  2. 计算进程标志在此过程中设置为1的次数,并将其保存在DX中。
  3. 假设这是正确的,我认为一个更好的代码将SHL中的值,S1倍数。 SHL AX, CX 但是,我无法想出一种方法来对进程中的进位进行求和。 (或计算原始AX的CX最高有效位中的' 1'位数。)

    非常感谢任何指导和帮助。

4 个答案:

答案 0 :(得分:2)

您对当前代码如何工作的理解基本上是正确的。为了确保我们理解它,让我们逐步执行示例。通常情况下,这种事情可以通过手头的调试器完成,但我们真正需要的只是我们的头和一个可以显示二进制值的计算器。

让我们说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包含迭代次数:

  • 隔离CX中的高AX位,丢弃其余位。
  • 计算AX
  • 中的设置位数

如果我们的目标是现代处理器 - 英特尔Nehalem,AMD巴塞罗那和更新 - 使用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

这将是快速。没有分支/循环;只有4个简单的说明。您在执行时间内只查看少数几个周期,不可能出现分支错误预测。

但是,如果您的目标是POPCNT指令不存在的旧CPU,该怎么办?好吧,你需要模仿它。实现种群计数/汉明加权算法有多种快速方法。在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优化此项,则需要使用查找表实现填充计数算法。而且,在这些处理器上,经常被遗忘的1字节XLAT指令是查找表中值的最快方法:

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个字节来存储代码中的查找表(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这样的简单芯片上,循环计数也不准确,但它确实为我们提供了一种估算性能的合理方法。您的原始代码确实具有稍好的最佳性能(在CX较小的情况下),但我们优化的,基于位计数,基于LUT的方法具有更好的最坏情况性能,更重要的是,更好地扩展。您可以在以下两种方法的图形比较中清楚地看到这一点:

Graph showing relative performance of the two approaches (using estimated cycle counts)

只要CX很小,您的原始代码就是合理的实现。但是随着CX越来越大,例程会因为所有LOOP而变得越来越慢。指数越来越慢。我们基于LUT的方法具有更大的开销(并且甚至不计算LUT添加到二进制文件中的膨胀),但真正开始得到回报,因为CX获得大。 总之,我们已经为执行速度增加了代码大小,这是一种常见的优化权衡。

现在,我需要干净。我一直偷偷地假设CX永远不会超过16,如果CX大于16,那么所有&#34;优化&#34;我一直在向您展示的代码无法正常工作,因为SHR指令会尝试移出太多位。如果您需要处理CX&gt; 16,然后你需要调整代码,使它钳位 CX小于或等于16。这意味着要么条件分支还是一系列聪明的位-twiddling指令,其中任何一个都会增加代码的复杂性并增加其循环次数。换句话说,这将增加&#34;优化&#34;的基线开销。方法,但这种方法将比原始方法更好地 scale 。从图形上看,红线将向上翻转。

(您的原始代码不需要进行任何修改 - 它会处理高达65,535的CX值而不会带来任何额外的惩罚,因为它只会保留LOOP。但是正如我们所做的那样已经看到,每个LOOP都会有显着的性能损失。)

&#34;调整&#34;代码看起来像这样:

    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个周期,而最坏情况下的性能约为两倍。

答案 1 :(得分:0)

使用非零cl假设,该指令对将是等效的:

   shld  dx,ax,cl
   shl   ax,cl

答案 2 :(得分:0)

如果您没有Dim price As Decimal If Decimal.TryParse(memberprices.RentalPrice, price) Then Dim strPrice As String = price.ToString("F2") ' .. use "strPrice" somehow ... Debug.Print(strPrice) End If (例如,因为您的代码必须在80286上运行,或者您想在非x86 CPU上使用相同的代码),您可以执行以下伪代码:

shld

答案 3 :(得分:0)

在其他答案中发布任何变化后,将移位的AX的高位置于DX中,您需要DX的一些变体(计算1位数)。执行此操作的代码有点冗长,下面显示了32位模式的示例。对于真正的8086,最好使用256字节表,将索引转换为索引中的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