それで、ビット操作に関して前にインタビューの質問がありました。同社は有名な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)
このコードが高速かどうかは、プロセッサに依存します。確かに、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命令 で行われ、NEONレジスタの各バイトのビットをカウントし、ビットカウントをD0のバイトに格納します。
vcnt.32
形式はなく、.8
のみなので、4バイトを水平方向に加算する必要があります。これが、残りのコードで行っていることです。
ビットハックの最良のリファレンスは
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
(または この素晴らしいオンラインgccツール )を使用して、この高レベルのスニペットが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 ..>
これはARMとタグ付けされているため、 clz
命令が最も役立ちます。この問題は、母集団数としても記述されます。 gcc
には __ builtin_popcount() があります。 ARMツール と同様です。 このリンク (あなたの解決策を気にしないでください、ほとんど同じものでウェブページを作成した人もいます)、そして Dave Seal's バージョンがあり、6つの指示がありますclz
以外のARM。 clz
有利 であり、入力に応じて、より高速なアルゴリズムを生成するために使用できます。
auselen's と同様に、優れた読書提案、ハッカーの喜び this bit twiddling blog おそらく、グラフィックコンテキストでそのようなことについて話すのに役立ちます。少なくとも、Qtのブリットコードのいくつかを理解することは役に立ちました。ただし、population countルーチンをコーディングするのに役立ちます。
_carry add
_ユニットは、分割統治の意味で役立ち、問題をO(ln n)
にします。 clz
は、データにonesまたはzeros。
Hacker's Delight エントリには、Dave SealのARMコードに関するより多くの背景があります。
long count_bits_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
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は、ADCを適切に機能させるための0のダミーレジスタです。