【问题标题】:Saving the bits that are shifted off in a left-shift (SHL)保存在左移 (SHL) 中移出的位
【发布时间】:2017-06-19 18:40:05
【问题描述】:

考虑以下 Intel 8086 汇编程序:

CX 持有非零值。

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

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

  1. 将 2^CX * AX 的值保存到 AX 中
  2. 统计进程中进位标志设置为 1 的次数,并将其保存在 DX 中。

假设这是正确的,我认为更好的代码会将 AX、CX 乘以 SHL 的值。 SHL AX, CX 但是,我想不出一种方法来对过程中的进位位求和。 (或计算原始 AX 的 CX 最高有效位中的 '1' 位数。)

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

【问题讨论】:

  • SHLD 或者,如果没有,自己合成。
  • SHL 将移位的位放入进位 (CF),因此您仍然可以使用 ADC reg, 0 来累积该位。但是SHL reg, cl 在 8086 上不会很快,所以这不会是优化。或者至少不是一个很好的。

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


【解决方案1】:

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

假设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) 位中设置 (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

这将是。没有分支/循环;只需 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

在代码中存储查找表 (LUT) 需要 256 个字节,但在 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 方法具有更好的最差-case 性能,更重要的是,它可以更好地扩展。您可以在以下两种方法的图形比较中清楚地看到这一点:

只要CX 很小,你的原始代码就是一个合理的实现。但是随着CX 变大,由于所有这些LOOPs,例程指数变得越来越慢。我们基于 LUT 的方法具有更大的开销(这甚至还没有计算 LUT 添加到二进制文件中的膨胀),但随着CX 变得更大,它确实开始得到回报。 总之,我们以增加代码大小换取执行速度,这是一种常见的优化权衡。

现在,我需要坦白。我一直在偷偷地假设CX 永远不会大于 16。如果CX 大于 16,那么我向您展示的所有“优化”代码都将不起作用,因为SHR 指令将尝试移出太多位。如果您需要处理 CX > 16,那么您需要调整代码以使其 钳位 CX 小于或等于 16。这意味着要么有条件分支或一系列巧妙的位旋转指令,其中任何一个都会增加代码的复杂性并增加其循环数。换句话说,这将增加“优化”方法的基线开销,但这种方法将继续扩展比您原来的方法更好。从图形上看,红线将向上平移。

(您的原始代码不需要任何修改 - 它可以处理高达 65,535 的 CX 值而没有任何额外的惩罚,因为它只是保持 LOOPing。但正如我们已经看到的那样,显着的性能损失是为每个LOOPs 付费。)

“调整”的代码如下所示:

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

【讨论】:

  • 我确实是在 CX
【解决方案2】:

在你的非零 cl 假设下,这个指令对是等价的:

   shld  dx,ax,cl
   shl   ax,cl

【讨论】:

  • 保罗,如果我没记错的话,第一条指令不会累积进位位。会吗? (即认为 '101' 是移位的位,我需要 DX 为 2 而不是 101)
  • 我不确定它有多重要,但shld是80386指令,OP写了8086
  • @Zoltan 道歉......我误读了这个问题,并以为我看到了 ADC DX,DX。我的回答是错误的(不等价的)。
【解决方案3】:

如果您没有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

【讨论】:

    【解决方案4】:

    在其他答案中发布的将移位 AX 的高位放入 DX 的任何变体之后,您将需要 DX 的 popcnt 的一些变体(计算 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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多