所以在关于位操作之前我有一个面试问题。该公司是一家知名的GPU公司。我在汇编语言方面的背景很少(虽然是计算机架构的博士生,但很奇怪),正如这个叙述所表明的那样,我把它搞砸了。问题很简单:
“编写一个快速代码,用于计算32位寄存器中1的数量。”
现在我正在研究手臂组装。所以我很自然地再次重新讨论这个问题,并通过研究ISA来提出这个代码。
对于你那里的专家来说,这是对的吗?有没有更快的方法呢?作为初学者,我自然认为这是不完整的。 “xx”中的AND指令感觉多余,但没有其他方法可以在ARM中移位寄存器......
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)
答案 0 :(得分:12)
如果此代码快或不快取决于处理器。当然,它在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寄存器中每个字节的位,并将bitcount存储回D0的字节。
没有vcnt.32
形式,只有.8
,因此您需要将4个字节水平添加到一起,这是其余代码正在执行的操作。
答案 1 :(得分:6)
比特黑客的最佳参考是
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
然后,我建议您使用gcc
和objdump
(或this great online gcc tool)来查看此高级代码段如何作为手臂指令。
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 ..>
答案 2 :(得分:5)
答案 3 :(得分:5)
由于这是标记的ARM,因此clz
指令最有帮助。该问题也被描述为人口计数。 gcc
为此__builtin_popcount()。与ARM tools一样。有this link(对你的解决方案感到不满意,有人创建了一个几乎相同的网页),还有Dave Seal's版本,其中包含非clz
ARM的6条指令。 clz
is advantageous可用于生成更快的算法,具体取决于输入。
除了auselen's良好的阅读建议, Hacker's Delight 这个bit twiddling blog也许有用,它在图形环境中讨论这些事情。至少我发现理解一些Qt的blitting代码很有用。但是,它在编码人口计数例程时有一些用处。
carry add
单位在划分和征服意义上很有用,可以解决问题O(ln n)
。如果数据运行 1 或零,clz
会更有用。
Hacker's Delight条目有更多关于Dave Seal的ARM代码的背景信息。
答案 4 :(得分:2)
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
答案 5 :(得分:0)
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工作正常。