13

1 バイトに 4 つの 2 ビット ビットフィールドが格納されています。したがって、各ビットフィールドは 0、1、2、または 3 を表すことができます。たとえば、最初の 3 つのビットフィールドがゼロである 4 つの可能な値は次のとおりです。

00 00 00 00 = 0 0 0 0
00 00 00 01 = 0 0 0 1
00 00 00 10 = 0 0 0 2
00 00 00 11 = 0 0 0 3

4 つのビットフィールドを効率的に合計する方法が必要です。例えば:

11 10 01 00 = 3 + 2 + 1 + 0 = 6

最新の Intel x64 CPU の 8 ビット ルックアップ テーブルは、L1 から応答を返すのに 4 サイクルかかります。それよりも速く答えを計算する方法があるはずです。3 サイクルで、6 ~ 12 の単純なビット操作を実行できます。スターターとして、単純なマスクとシフトは、Sandy Bridge で 5 サイクルかかるように見えます。

d c b aビット フィールドがであり、そのマスクが次のとおりであると仮定します。00 00 00 11

Ira の助けを借りて明確化: これは、abc、およびdが同一であり、すべて初期値 に設定されていることを前提としていbyteます。奇妙なことに、私はこれを無料で行うことができると思います。サイクルごとに 2 回のロードを実行できるためbyte、1 回のロードではなく 4 回ロードできます。つまり、最初のサイクルaと2 番目のサイクルです。2 番目の 2 つのロードは 1 サイクル遅れますが、2 番目のサイクルまでは必要ありません。以下の分割は、物事がどのように個別のサイクルに分割されるかを表しています。dbc

a = *byte
d = *byte

b = *byte
c = *byte

latency

latency

a &= mask
d >>= 6

b >>= 2
c >>= 4
a += d

b &= mask
c &= mask

b += c

a += b

ロジックを簡単にするためのビットフィールドの別のエンコーディングは、それが 1 バイトに収まり、このスキームで 1 対 1 でマップされる限り、実際には問題ありません。組み立てへの落とし込みもOK。現在のターゲットは Sandy Bridge ですが、Haswell 以降をターゲットにすることもできます。

アプリケーションと動機: オープン ソースの可変ビット圧縮解除ルーチンを高速化しようとしています。各ビットフィールドは、後続の 4 つの整数それぞれの圧縮された長さを表します。次の 4 つのグループに到達するためにジャンプする必要があるバイト数を知るには、合計が必要です。現在のループには 10 サイクルかかり、そのうちの 5 サイクルは回避しようとしているルックアップです。サイクルを削ると、最大 10% の改善になります。

編集:

もともと「8 サイクル」と言いましたが、Evgeny が以下で指摘しているように、私は間違っていました。Evgeny が指摘しているように、間接的な 4 サイクル ロードが発生するのは、インデックス レジスタを使用せずにシステム メモリの最初の 2K からロードする場合のみです。レイテンシーの正確なリストは、インテル アーキテクチャー最適化マニュアルのセクション 2.12にあります。

>    Data Type       (Base + Offset) > 2048   (Base + Offset) < 2048 
>                     Base + Index [+ Offset]
>     Integer                5 cycles               4 cycles
>     MMX, SSE, 128-bit AVX  6 cycles               5 cycles
>     X87                    7 cycles               6 cycles 
>     256-bit AVX            7 cycles               7 cycles

編集:

これが、以下のIraのソリューションがサイクルに分割される方法だと思います。ワークポストロードにも5サイクルかかると思います。

a = *byte
b = *byte

latency

latency 

latency

a &= 0x33
b >>= 2

b &= 0x33
c = a

a += b
c += b

a &= 7
c >>= 4

a += c 
4

6 に答える 6

4

検討

 temp = (byte & 0x33) + ((byte >> 2) & 0x33);
 sum = (temp &7) + (temp>>4);

9 つのマシン命令である必要があり、それらの多くは並行して実行されます。(OP の最初の試行は、9 つ​​の命令と、言及されていないいくつかの動きです)。

調べてみると、シリアル依存関係が多すぎてうまくいかないようです。

編集: Binary-ops が破壊的であり、LEA がこれを回避することについての議論により、LEA を使用して複数のオペランドを組み合わせ、定数による乗算を行う方法について考えるようになりました。上記のコードは、右にシフトすることで答えを正規化しようとしますが、乗算することで答えを左正規化できます。その洞察があれば、次のコードが機能する可能性があります。

     mov     ebx,  byte      ; ~1: gotta start somewhere
     mov     ecx, ebx        ; ~2: = byte
     and     ebx, 0xCC       ; ~3: 2 sets of 2 bits, with zeroed holes
     and     ecx, 0x33       ; ~3: complementary pair of bits
     lea     edx, [ebx+4*ecx] ; ~4: sum bit pairs, forming 2 4-bit sums
     lea     edi, [8*edx+edx] ; ~5: need 16*(lower bits of edx)
     lea     edi, [8*edx+edi] ; ~6: edi = upper nibble + 16* lower nibble
     shr     edi, 4           ; ~7: right normalized
     and     edi, 0x0F        ; ~8: masked

うーん、面白いが、まだうまくいかなかった. 3 クロックはそれほど長くはありません :-{

于 2013-07-26T12:02:51.170 に答える
3

どれくらいのサイクルがかかるかわかりませんし、完全にオフになっているかもしれませんが、32 ビットの乗算を使用して 5 つの単純な演算を合計することは可能です。

unsigned int sum = ((((byte * 0x10101) & 0xC30C3) * 0x41041) >> 18) & 0xF;

最初の乗算はビット パターンを繰り返します

abcdefgh -> abcdefghabcdefghabcdefgh

最初のビットは、6 ビットごとにペアを保持します。

abcdefghabcdefghabcdefgh -> 0000ef0000cd0000ab0000gh

2 番目の乗算は、ビット パターンを合計します (yyyy のみが重要です)。

                     0000ef0000cd0000ab0000gh
             + 0000ef0000cd0000ab0000gh000000
       + 0000ef0000cd0000ab0000gh000000000000
 + 0000ef0000cd0000ab0000gh000000000000000000
 --------------------------------------------
   ..................00yyyy00................

最後の 2 つの ops は yyyy を右にシフトし、左の部分を切り取ります

主な問題は、ops がシーケンシャルであることですが...

編集

または、全体を 10 ビット左に変換し、最後のビットを削除します。

unsigned int sum = (((byte * 0x4040400) & 0x30C30C00) * 0x41041) >> 28;
于 2013-07-30T21:50:53.450 に答える
2

ここには素晴らしいアイデアがたくさんありますが、議論の中でそれらを見つけるのは難しくなっています. この回答を使用して、最終的な解決策とそのタイミングを提供しましょう。この投稿を自由に編集し、タイミングに合わせて独自の投稿を追加してください。タイミングが不明な場合は、下部のコードに貼り付けてください。測定します。x64 アセンブリが最適です。喜んで C をコンパイルしますが、このレベルの最適化では、多くの微調整なしで良い結果が得られることはめったにありません。

概要

質問を適切なコンテキストに言い換える: 目標は、「Varint-GB」(または Group Varint) で知られる整数圧縮形式を迅速にデコードすることです。Daniel Lemire と Leo Boytsov による論文で説明されています。. 私は標準的な「明らかに著者はばかだ」スタイルのこの論文の最初のバージョンについてコメントし、Daniel (この論文の主な著者であり、それほどばかではない) は、フォローアップのためのコード作成を助けるために狡猾に私を巻き込んだ.

標準の Varint (別名 VByte) には、各バイトの先頭に整数の末尾かどうかを判断するフラグがありますが、これは解析に時間がかかります。このバージョンには、1 バイトの「キー」と、ペイロードの 4 つの圧縮された整数があります。キーは 4 つの 2 ビット フィールドで構成され、それぞれが後続の圧縮された整数のバイト長を表します。それぞれ、1 バイト (00)、2 バイト (01)、3 バイト (10)、または 4 バイト (11) にすることができます。したがって、各「チャンク」は 5 ~ 17 バイトの長さですが、常に同じ数 (4) の 32 ビット符号なし整数をエンコードします。

Sample Chunk:
  Key:  01 01 10 00  
  Data: [A: two bytes] [B: two bytes] [C: three bytes] [D: one byte]
Decodes to: 00 00 AA AA   00 00 BB BB   00 CC CC CC  00 00 00 DD

キーは、16 バイトのシャッフル パターンのテーブルへのインデックスであり、実際のデコードは、PSHUFB を使用してデータ バイトを正しい間隔にシャッフルすることによって行われます。

vec_t Data = *input
vec_t ShuffleKey = decodeTable[key]     
VEC_SHUFFLE(Data, ShuffleKey) // PSHUFB
*out = Data

実際には、通常、元の整数は通常、整数自体ではなく整数間の「デルタ」(差) を圧縮することによって小さくされているため、通常は「デルタ復号化」ステップもあります。ただし、デコード ルーチンのレイテンシは、次の反復がそれに依存しないため、通常は問題になりません。

問題の再掲

ここで指定されている問題は、ある「キー」から次のキーにホップすることです。ここでは、デコードされたデータには依存関係がないため (キーのみ)、実際のデコードは無視して、キーを読み取るループに集中します。この関数は、キーへのポインタとカウント n を取り、n 番目のキーを返します。

11サイクル

「基本的な」アプローチは、キーをインデックスとして「事前」オフセットのルックアップ テーブルを使用することです。offsetTable 内の 256 個のキーのいずれかを検索して、事前に計算された (合計 + 1) オフセットを取得します。それを現在の入力位置に追加し、次のキーを読み取ります。Intel の IACA によると、このループは Sandy Bridge で 11 サイクルかかります (Sandy Bridge でも 1 サイクルはそれ以下にカウントされます)。

uint32_t decodeBasic(uint8_t *in, size_t count) {
    uint64_t key, advance;
    for (size_t i = count; i > 0; i--) {
        key = *in;
        advance = offsetTable[key];
        in += advance;
    }
    return key;
}

0000000000000000 <decodeBasic>:
   0:   test   %rsi,%rsi
   3:   je     19 <decodeBasic+0x19>
   5:   nopl   (%rax)
   8:   movzbl (%rdi),%eax
   b:   add    0x0(,%rax,8),%rdi
  13:   sub    $0x1,%rsi
  17:   jne    8 <decodeBasic+0x8>
  19:   repz retq 

Block Throughput: 11.00 Cycles       Throughput Bottleneck: InterIteration
   0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
--------------------------------------------------------------
|           |     | 1.0   1.0 |           |     |     | CP | movzx eax, byte ptr [rdi]
| 0.3       | 0.3 |           | 1.0   1.0 |     | 0.3 | CP | add rdi, qword ptr [rax*8]
|           |     |           |           |     | 1.0 |    | sub rsi, 0x1
|           |     |           |           |     |     |    | jnz 0xffffffffffffffe7

10サイクル

そこから、入力ポインターの更新を追加し、同時に次のキーの読み込みを開始するようにループを再配置することで、10 サイクルまで下げることができます。インライン アセンブリを使用して、コンパイラが必要な出力を生成するように「奨励」しなければならなかったことに気付くかもしれません。外側のループも (通常は) 同じままなので、削除し始めます。

key = *in;
advance = offsetTable[key]; 
for (size_t i = count; i > 0; i--) {
    key = *(in + advance);
    ASM_LEA_ADD_BASE(in, advance);
    advance = offsetTable[key];
}

Block Throughput: 10.00 Cycles       Throughput Bottleneck: InterIteration
|  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
------------------------------------------------------------
|           |     | 1.0   1.0 |           |     |     | CP | movzx eax, byte ptr [rdi+rdx*1]
| 0.5       | 0.5 |           |           |     |     |    | lea rdi, ptr [rdi+rdx*1]
|           |     |           | 1.0   1.0 |     |     | CP | mov rdx, qword ptr [rax*8]
|           |     |           |           |     | 1.0 |    | sub rsi, 0x1
|           |     |           |           |     |     |    | jnz 0xffffffffffffffe2

9サイクル

以前に POPCNT を使用しようとしましたが、Ira と AShelly からの提案、ヒント、およびアイデアがなければ、うまくいきませんでした。しかし、ピースをまとめると、9 サイクルでループを実行するものがあると思います。実際のデコーダーに入れてみましたが、Ints/s の数はこれと一致しているようです。このループは本質的にアセンブリにあります。複数のコンパイラは別として、他の方法では望んでいたことをコンパイラに実行させることができなかったからです。

[編集: AShelly によるコメントごとに余分な MOV を削除]

uint64_t key1 = *in;
uint64_t key2 = *in;
for (size_t i = count; i > 0; i--) {
    uint64_t advance1, advance2;
    ASM_POPCOUNT(advance1, key1);
    ASM_AND(key2, 0xAA);

    ASM_POPCOUNT(advance2, key2);
    in += advance1;

    ASM_MOVE_BYTE(key1, *(in + advance2 + 4));
    ASM_LOAD_BASE_OFFSET_INDEX_MUL(key2, in, 4, advance2, 1);        
    in += advance2;
 }


Block Throughput: 9.00 Cycles       Throughput Bottleneck: InterIteration
|  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
------------------------------------------------------------
|           | 1.0 |           |           |     |     | CP | popcnt r8, rax
| 1.0       |     |           |           |     |     | CP | and rdx, 0xaa
|           |     |           |           |     | 1.0 | CP | add r8, rdi
|           | 1.0 |           |           |     |     | CP | popcnt rcx, rdx
|           |     | 1.0   1.0 |           |     |     | CP | movzx rax, byte ptr [rcx+r8*1+0x4]
|           |     |           | 1.0   1.0 |     |     | CP | mov rdx, qword ptr [r8+rcx*1+0x4]
| 1.0       |     |           |           |     |     |    | lea rdi, ptr [rcx+r8*1]
|           |     |           |           |     | 1.0 |    | dec rsi
|           |     |           |           |     |     |    | jnz 0xffffffffffffffd0

最新のプロセッサーの可動部分の複雑さを示すものとして、私はこのルーチンのバリエーションで興味深い経験をしました。と ( ) でメモリの場所を指定mov raxして 2 行目を結合すると、30% 実行するために実行が不安定なルーチンになってしまいました。これは、ループに至るまでの初期条件によって、load/and の「and」マイクロ操作がループ全体の POPCNT の前に実行されることがあるからだと思います。and rax, 0xaamov rax, 0xAA; and rax, qword ptr [r8+rcx*1+0x4]

8サイクル

誰?

エフゲニー

これは、エフゲニーのソリューションを実装する私の試みです。少なくとも IACA の Sandy Bridge のモデル (これまでのところ正確です) については、まだ 9 サイクルまで下げることができませんでした。問題は、ROR のレイテンシは 1 ですが、P1 または P5 で 2 つのマイクロオペレーションが必要になることだと思います。レイテンシを 1 にするには、両方が利用可能である必要があります。AND、ADD、および MOV は P0、P1、または P5 で発行できますが、SHR は P1 で発行できません。ADD と AND が SHR または ROR を置き換えるのを防ぐ余分なジャンク操作を追加することで、10 サイクルに近づけることができますが、10 を下回る方法はわかりません。

Block Throughput: 10.55 Cycles       Throughput Bottleneck: InterIteration
|  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
------------------------------------------------------------
|           |     | 1.0   1.0 |           |     |     | CP | movzx eax, byte ptr [esi+0x5]
|           |     |           | 1.0   1.0 |     |     | CP | movzx ebx, byte ptr [esi+0x5]
| 0.2       | 0.6 |           |           |     | 0.3 |    | add esi, 0x5
| 0.3       | 0.3 |           |           |     | 0.3 |    | mov ecx, 0x3
| 0.2       | 0.2 |           |           |     | 0.6 |    | mov edx, 0x3
| 1.4       |     |           |           |     | 0.6 | CP | ror al, 0x4
| 0.1       | 0.7 |           |           |     | 0.2 | CP | and ecx, ebx
| 0.6       |     |           |           |     | 0.4 | CP | shr ebx, 0x6
| 0.1       | 0.7 |           |           |     | 0.2 | CP | add ebx, ecx
| 0.3       | 0.4 |           |           |     | 0.3 | CP | and edx, eax
| 0.6       |     |           |           |     | 0.3 | CP | shr eax, 0x6
| 0.1       | 0.7 |           |           |     | 0.2 | CP | add eax, edx
| 0.3       | 0.3 |           |           |     | 0.3 | CP | add esi, ebx
| 0.2       | 0.2 |           |           |     | 0.6 | CP | add esi, eax
于 2013-07-30T02:32:57.577 に答える
0
  mov al,1
  mov ah,2
  mov bl,3
  mov bh,4
  add ax,bx
  add al,ah
于 2013-07-30T08:01:10.013 に答える