【问题标题】:Fastest way to count number of 1s in a register, ARM assembly计算寄存器中 1 数量的最快方法,ARM 程序集
【发布时间】:2013-03-22 02:57:58
【问题描述】:

所以我之前有一个关于位操作的面试问题。该公司是一家知名的 GPU 公司。我在汇编语言方面的背景很少(尽管是计算机体系结构的博士生,但很奇怪),正如这个叙述所表明的那样,我搞砸了。问题很简单:

“编写一个快速代码,计算 32 位寄存器中 1 的数量。”

现在我正在研究手臂组装。所以很自然地,我再次重新审视了这个问题,并通过研究 ISA 得出了这段代码。

对于你们那里的武装专家来说,这是正确的吗?有没有更快的方法来做到这一点?作为一个初学者,我自然认为这是不完整的。 "xx" 中的 AND 指令感觉是多余的,但是在 ARM isa 中没有其他方法可以移位寄存器...

R1 将包含最后的位数,而 R2 是我们要计数的位的寄存器。 r6 只是一个虚拟寄存器。注释用 () 括起来

    MOV   R1, #0                (initialize R1 and R6 to zero)
    MOV   R6, #0        
xx: AND   R6, R6, R2, LSR #1    (Right shift by 1, right most bit is in carry flag)
    ADDCS R1, #1                (Add #1 to R1 if carry  flag is set)
    CMP R2, #0                  (update the status flags if R2 == 0 or not)
    BEQ xx                      (branch back to xx until R2==0)

【问题讨论】:

  • 最快的还取决于您希望设置的位数。很少有 1,Kernighan's bit counter 将获胜,因为它为每个位集运行一轮循环。
  • 您的算法对所有1 都采用4*32+2 指令。 Dave Seal 的算法需要六条指令加上一次内存访问;除非内存非常慢,否则它可能对所有类型的输入都一样快。我怀疑很少有人会得到 Dave Seal 的解决方案。 reverse subtract 对我来说很奇怪。

标签: assembly arm


【解决方案1】:

此代码是否快速取决于处理器。可以肯定的是,它在 Cortex-A8 上不会很快,但在 Cortex-A9 和更新的 CPU 上可能会运行得非常快。

然而,这是一个非常简短的解决方案。

期望在 r0 中输入,并在 r0 中返回输出

  vmov.32 d0[0], r0
  vcnt.8  d0, d0
  vmov.32 r0, d0[0]

  add r0, r0, r0, lsr #16
  add r0, r0, r0, lsr #8
  and r0, r0, #31

主要工作在vcnt.8 instruction 中完成,它计算 NEON 寄存器中每个字节的位数,并将位数存储回 D0 的字节中。

没有vcnt.32形式,只有.8,所以需要将4个字节水平相加,这就是其余代码的作用。

【讨论】:

  • vmov.32 r0, d0[0] 在 NEON 和 Core 之间造成非常大的同步延迟
【解决方案2】:

由于它被标记为 ARM,clz 指令最有帮助。该问题也被描述为人口计数gcc 对此有 __builtin_popcount()ARM tools 也是如此。有this link(不要对您的解决方案感到难过,有人制作了一个几乎相同的网页)还有Dave Seal's 版本,其中包含非clz ARM 的六条指令。 clz is advantageous 可用于生成更快的算法,具体取决于输入。

除了auselen's 很好的阅读建议,Hacker's Delight 这个bit twiddling blog 在图形上下文中讨论这些事情可能会有所帮助。至少我发现理解一些 Qt 的 blitting 代码很有用。但是,它在编写人口计数例程时有一些用处。

carry add 单元在分而治之的意义上很有用,这导致了O(ln n) 的问题。如果数据有 oneszeros 运行,clz 会更有用。

Hacker's Delight 条目有更多关于 Dave Seal 的 ARM 代码的背景信息。

【讨论】:

  • 我认为 __builtin_popcount 是从查找表实现编译而来的,它应该足够好,但没有专门为 arm 设计的。
  • @auselen:检查您所针对的 ARM 架构级别。 clz 仅从 ARMv5 开始可用。
  • @IgorSkochinsky 我认为gccllvm 没有提供clz 实现。 __builtin_popcount() 链接到__popcountsi2
  • 好吧,popcount 比clz 做得更多,所以它需要一个辅助例程。 __builtin_clz__builtin_ctz 直接为支持的架构发出 clz
  • 例如,二进制图像/图片数据上的人口计数可能会受益于clz,特别是如果它有很多低频内容。我认为这个问题是在图形的背景下提出的。
【解决方案3】:

bit hack 的最佳参考是

Bit Twiddling Hacks 页面说

The best method for counting bits in a 32-bit
integer v is the following:

v = v - ((v >> 1) & 0x55555555);                    // reuse input as temporary
v = (v & 0x33333333) + ((v >> 2) & 0x33333333);     // temp
c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; // count

那么我建议你使用gccobjdump(或this great online gcc tool)来看看这个高级sn-p 看起来像arm 指令。

00000000 <popcount>:
 0: 1043        asrs    r3, r0, #1
 2: f003 3355   and.w   r3, r3, #1431655765 ; 0x55555555
 6: 1ac0        subs    r0, r0, r3
 8: 1083        asrs    r3, r0, #2
 a: f000 3033   and.w   r0, r0, #858993459  ; 0x33333333
 e: f003 3333   and.w   r3, r3, #858993459  ; 0x33333333
12: 18c0        adds    r0, r0, r3
14: eb00 1010   add.w   r0, r0, r0, lsr #4
18: f000 300f   and.w   r0, r0, #252645135  ; 0xf0f0f0f
1c: eb00 2000   add.w   r0, r0, r0, lsl #8
20: eb00 4000   add.w   r0, r0, r0, lsl #16
24: 1600        asrs    r0, r0, #24
26: 4770        bx  lr

所以看起来这会给您带来12 指令的结果,大致可以转换为相同数量的周期。

将上面的整数旋转与libgcc 使用的look up table 方法进行比较,考虑到额外的内存访问,查找表应该会更慢。

00000028 <__popcountSI2>:
28: b410        push    {r4}
2a: 2200        movs    r2, #0
2c: 4c06        ldr r4, [pc, #24]   ; (48 <__popcountSI2+0x20>)
2e: 4613        mov r3, r2
30: fa40 f103   asr.w   r1, r0, r3
34: 3308        adds    r3, #8
36: 2b20        cmp r3, #32
38: b2c9        uxtb    r1, r1
3a: 5c61        ldrb    r1, [r4, r1]
3c: 440a        add r2, r1
3e: d1f7        bne.n   30 <__popcountSI2+0x8>
40: 4610        mov r0, r2
42: bc10        pop {r4}
44: 4770        bx  lr
46: bf00        nop
48: 00000000    andeq   r0, r0, r0
<.. snipped ..>

【讨论】:

  • 如何将 32 位常量放入 32 位指令中?
  • @LưuVĩnhPhúc 说什么?
  • 像这样and.w r0, r0, #252645135 ; 0xf0f0f0f常量的大小是32位
  • @LưuVĩnhPhúc 好吧,那是编译器的输出,所以我认为这是很有可能的。想法是,如果常数具有低熵,您可以将其填充到指令中。应该有一种有效的方法将 0x80000000 加载到寄存器中,否则它将是一个糟糕的 ISA。
  • and.w 指令不是普通的 AND,您可以从操作码条件字段中的 f 前缀中看出。在这种情况下,它被赋予一个单字节的立即数 0x0f,它适用于寄存器输入的所有 4 个字节,实际上是 0x0f0f0f0f
【解决方案4】:

您可以使用预先计算的查找表并将迭代次数减少到 2 或 4。

您也可以使用对数方法。

欲了解更多信息,请参阅this Wikipedia article

【讨论】:

【解决方案5】:
    LDR r0, = 0x000000FF;
    MOV r1, #0;
    MOV r3, #0; this will always be zero
    MOV r2,r0;
rep MOVS r2, r2, LSR #1;
    ADC r1,r1, r3;  this adds r1 with zero plus the carry bit
    CMP r2, #0;
    BNE rep

这样就可以了,r3只是一个虚寄存器,0为使ADC正常工作。

【讨论】:

  • 一次移动一位并不快。
  • @Avsharian:“CMP”在这里是多余的(你明白为什么吗?)。与 r3 相同:#0 足以“使 ADC 正常工作”(至于最初的“LDR”,我只是想知道......)。如果你在汇编中写作,请留意,至少在 StackOverflow 上:)
【解决方案6】:

long count_bits_long (long);

    vmov.32 d0[0], r0       // R0 --> SIMD

    vcnt.8  d0, d0          // count bits in bytes
    vpaddl.u8 d0, d0        // add adjacent pairs of bytes and put into 16b words
    vpaddl.u16 d0, d0       // add adjacent pairs of 16b words and put into 32b word

    vmov.32 r0, d0[0]       // SIMD --> R0

    mov pc, lr              // return

【讨论】:

  • 这是@Nils 的答案,但NEON 水平总和而不是标量。这在哪些 CPU 上更快?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-28
  • 2013-08-22
  • 2012-02-24
  • 1970-01-01
  • 2013-06-25
  • 1970-01-01
相关资源
最近更新 更多