ビットストリームで16ビットワードをスキャンする必要があります。 バイトまたはワードの境界に整列することは保証されていません。
これを達成するための最速の方法は何ですか?さまざまな強引な方法があります。テーブルやシフトを使用していますが、yes/noを指定することで計算の数を減らすことができる「ビットをいじるショートカット」はありますか?おそらく、到着した各バイトまたはWordのフラグ結果が含まれていますか?
Cコード、組み込み関数、x86マシンコードはすべて興味深いものです。
単純なブルートフォースを使用するとよい場合があります。
Wordのシフトされたすべての値を事前に計算し、それらを16 intに入れると、次のような配列が得られると思います(int
の幅がshort
の2倍であると仮定)
unsigned short pattern = 1234;
unsigned int preShifts[16];
unsigned int masks[16];
int i;
for(i=0; i<16; i++)
{
preShifts[i] = (unsigned int)(pattern<<i); //gets promoted to int
masks[i] = (unsigned int) (0xffff<<i);
}
次に、ストリームから出るすべてのunsigned shortについて、そのshortと前のshortのintを作成し、そのunsignedintを16のunsignedintと比較します。それらのいずれかが一致する場合、あなたは1つを手に入れました。
だから基本的にこのように:
int numMatch(unsigned short curWord, unsigned short prevWord)
{
int numHits = 0;
int combinedWords = (prevWord<<16) + curWord;
int i=0;
for(i=0; i<16; i++)
{
if((combinedWords & masks[i]) == preShifsts[i]) numHits++;
}
return numHits;
}
パターンが同じビットで複数回検出された場合、これは複数のヒットを意味する可能性があることに注意してください。
例えば32ビットの0で、検出するパターンが16 0の場合、パターンが16回検出されることを意味します。
これの時間コストは、ほぼ記述どおりにコンパイルされると仮定すると、入力ワードごとに16チェックです。入力ビットごとに、これは1つの&
および==
、および分岐またはその他の条件付きインクリメント。また、すべてのビットのマスクのテーブルルックアップ。
テーブルルックアップは不要です。代わりにcombined
を右シフトすることで、次のように大幅に効率的なasmが得られます 別の回答 これは、x86でSIMDを使用してこれをベクトル化する方法も示しています。
2文字のアルファベット{0、1}のクヌース-モリス-プラットアルゴリズムもレイニエルのアイデアも十分に高速でない場合、検索を32倍高速化するためのトリックがあります。
最初に256エントリのテーブルを使用して、ビットストリームの各バイトが探している16ビットワードに含まれているかどうかを確認できます。あなたが得るテーブル
unsigned char table[256];
for (int i=0; i<256; i++)
table[i] = 0; // initialize with false
for (i=0; i<8; i++)
table[(Word >> i) & 0xff] = 1; // mark contained bytes with true
次に、を使用してビットストリーム内の一致の可能な位置を見つけることができます
for (i=0; i<length; i++) {
if (table[bitstream[i]]) {
// here comes the code which checks if there is really a match
}
}
256個のテーブルエントリのうち最大8個がゼロではないため、平均して32番目ごとの位置のみを詳しく調べる必要があります。このバイト(前後のバイトと組み合わせて)についてのみ、ビット演算またはreinierによって提案されたいくつかのマスキング手法を使用して、一致するかどうかを確認する必要があります。
このコードは、リトルエンディアンのバイトオーダーを使用することを前提としています。バイト内のビットの順序も問題になる可能性があります(CRC32チェックサムをすでに実装しているすべての人に知られています)。
サイズ256の3つのルックアップテーブルを使用するソリューションを提案したいと思います。これは、大きなビットストリームに対して効率的です。このソリューションは、比較のためにサンプルで3バイトを取ります。次の図は、3バイトの16ビットデータのすべての可能な配置を示しています。各バイト領域は異なる色で表示されています。
代替テキストhttp://img70.imageshack.us/img70/8711/80541519.jpg
ここでは、最初のサンプルで1〜8、次のサンプルで9〜16のチェックが行われます。 Patternを検索すると、このPatternの8つの可能な配置すべてが見つかり、3つのルックアップテーブル(Left、Middle、および正しい)。
ルックアップテーブルの初期化:
0111011101110111
をPatternとして例を挙げてみましょう。次に、4番目の配置について考えます。左側はXXX01110
になります。左側の部分(XXX01110
)を指す左側のルックアップテーブルのすべてのrawを00010000
で埋めます。 1は入力の配置の開始位置を示しますパターン。したがって、左ルックアップテーブルの次の8つのrawは、16(00010000
)で埋められます。
00001110
00101110
01001110
01101110
10001110
10101110
11001110
11101110
配置の中間部分は11101110
になります。ミドルルックアップテーブルのこのインデックス(238)による生のポインティングは、16(00010000
)で埋められます。
これで、配置の正しい部分は111XXXXX
になります。インデックスが111XXXXX
のすべてのraw(32 raw)は、16(00010000
)で埋められます。
充填中にルックアップテーブルの要素を上書きしないでください。代わりに、ビット単位のOR操作を実行して、すでに入力されているrawを更新します。上記の例では、3番目の配置で書き込まれたすべてのrawは、次のように7番目の配置で更新されます。
したがって、左ルックアップテーブルにインデックスXX011101
、中央ルックアップテーブルに11101110
、右ルックアップテーブルに111XXXXX
のrawは、7番目の配置によって00100010
に更新されます。
検索パターン:
3バイトのサンプルを取ります。 Countを次のように検索します。ここで、Leftは左ルックアップテーブル、Middleは中央ルックアップテーブル、Rightは右ルックアップテーブルです。 。
Count = Left[Byte0] & Middle[Byte1] & Right[Byte2];
Countの1の数は、取得したサンプルの一致するPatternの数を示します。
テストされたサンプルコードをいくつかあげることができます。
ルックアップテーブルの初期化:
for( RightShift = 0; RightShift < 8; RightShift++ )
{
LeftShift = 8 - RightShift;
Starting = 128 >> RightShift;
Byte = MSB >> RightShift;
Count = 0xFF >> LeftShift;
for( i = 0; i <= Count; i++ )
{
Index = ( i << LeftShift ) | Byte;
Left[Index] |= Starting;
}
Byte = LSB << LeftShift;
Count = 0xFF >> RightShift;
for( i = 0; i <= Count; i++ )
{
Index = i | Byte;
Right[Index] |= Starting;
}
Index = ( unsigned char )(( Pattern >> RightShift ) & 0xFF );
Middle[Index] |= Starting;
}
検索パターン:
Dataはストリームバッファ、Leftは左ルックアップテーブル、Middleは中央ルックアップテーブル、Rightは右ルックアップテーブルです。 。
for( int Index = 1; Index < ( StreamLength - 1); Index++ )
{
Count = Left[Data[Index - 1]] & Middle[Data[Index]] & Right[Data[Index + 1]];
if( Count )
{
TotalCount += GetNumberOfOnes( Count );
}
}
制限:
上記のループは、ストリームバッファの最後に配置されている場合、Patternを検出できません。この制限を克服するには、次のコードでafterループを追加する必要があります。
Count = Left[Data[StreamLength - 2]] & Middle[Data[StreamLength - 1]] & 128;
if( Count )
{
TotalCount += GetNumberOfOnes( Count );
}
利点:
このアルゴリズムは、N-1論理的な手順で[〜#〜] n [〜#〜]バイトの配列からパターンを見つけるだけです。 。オーバーヘッドは、すべての場合で一定であるルックアップテーブルを最初に埋めることだけです。したがって、これは巨大なバイトストリームの検索に非常に効果的です。
私のお金は Knuth-Morris-Pratt 2文字のアルファベットで。
16の状態を持つステートマシンを実装します。
各状態は、パターンに準拠する受信ビット数を表します。次の受信ビットがパターンの次のビットに一致する場合、マシンは次の状態に進みます。そうでない場合、マシンは最初の状態に戻ります(または、パターンの先頭をより少ない数の受信ビットと一致させることができる場合は、別の状態に戻ります)。
マシンが最後の状態に達すると、これはパターンがビットストリームで識別されたことを示します。
アトミックの
詳細についての詳細についてのルークとMSalterの要求を検討するまでは良さそうだった。
詳細は、KMPよりも迅速なアプローチを示している可能性があります。 KMPの記事はにリンクしています
検索パターンが「AAAAAA」の特定の場合。複数パターン検索の場合、
最適かもしれません。
あなたはさらなる入門的な議論を見つけることができます ここ 。
汎用の非SIMDアルゴリズムの場合、次のようなものよりもはるかに優れた処理を実行できる可能性はほとんどありません。
unsigned int const pattern = pattern to search for
unsigned int accumulator = first three input bytes
do
{
bool const found = ( ((accumulator ) & ((1<<16)-1)) == pattern )
| ( ((accumulator>>1) & ((1<<16)-1)) == pattern );
| ( ((accumulator>>2) & ((1<<16)-1)) == pattern );
| ( ((accumulator>>3) & ((1<<16)-1)) == pattern );
| ( ((accumulator>>4) & ((1<<16)-1)) == pattern );
| ( ((accumulator>>5) & ((1<<16)-1)) == pattern );
| ( ((accumulator>>6) & ((1<<16)-1)) == pattern );
| ( ((accumulator>>7) & ((1<<16)-1)) == pattern );
if( found ) { /* pattern found */ }
accumulator >>= 8;
unsigned int const data = next input byte
accumulator |= (data<<8);
} while( there is input data left );
SIMD命令の良い使い方のようです。 SSE2は、複数の整数を同時にクランチするための一連の整数命令を追加しましたが、データが整列されないため、多くのビットシフトを伴わないこれに対する多くのソリューションを想像することはできません。これは実際にはFPGAが実行すべきことのように聞こえます。
非常に大きな入力(nの値)に対して高速フーリエ変換を使用して、O(n log n)時間のanyビットパターンを見つけることができます。ビットマスクと入力の相互相関を計算します。シーケンスxとマスクyの相互相関は、それぞれサイズnとn 'で定義されます。
R(m) = sum _ k = 0 ^ n' x_{k+m} y_k
次に、マスクに正確に一致するビットパターンの出現R(m) = Yここで、Yはビットマスク内の1の合計です。
したがって、ビットパターンを一致させようとしている場合
[0 0 1 0 1 0]
に
[ 1 1 0 0 1 0 1 0 0 0 1 0 1 0 1]
その後、マスクを使用する必要があります
[-1 -1 1 -1 1 -1]
マスクの-1は、それらの場所が0でなければならないことを保証します。
O(n log n)時間のFFTを使用して、相互相関を実装できます。
KMPにはO(n + k)のランタイムがあると思うので、これを打ち負かします。
私がすることは、16個のプレフィックスと16個のサフィックスを作成することです。次に、16ビットの入力チャンクごとに、最も長いサフィックスの一致を決定します。次のチャンクに長さ(16-N)
のプレフィックス一致がある場合、一致します。
接尾辞の一致は、実際には16回の比較ではありません。ただし、これにはパターンWordに基づいた事前計算が必要です。たとえば、パターンワードが101010101010101010の場合、最初に16ビット入力チャンクの最後のビットをテストできます。そのビットが0の場合、テストする必要があるのは... 10101010で十分です。最後のビットが1の場合、... 1010101で十分にテストする必要があります。それぞれ8つあり、合計1 +8の比較になります。パターンワードが1111111111110000の場合でも、入力の最後のビットでサフィックスの一致をテストします。そのビットが1の場合、12回のサフィックス一致(正規表現:1 {1,12})を実行する必要がありますが、0の場合、可能な一致は4回のみです(正規表現1111 1111 1111 0 {1,4})。 9つのテストの。 16-N
プレフィックス一致を追加すると、16ビットチャンクごとに10回のチェックのみが必要であることがわかります。
たぶん、ベクトル(vec_str)でビットストリームをストリーミングし、別のベクトル(vec_pattern)でパターンをストリーミングしてから、以下のアルゴリズムのようなことを行う必要があります。
i=0
while i<vec_pattern.length
j=0
while j<vec_str.length
if (vec_str[j] xor vec_pattern[i])
i=0
j++
(アルゴリズムが正しいことを願っています)
@ Toadのすべてのビット位置をチェックする単純なブルートフォースアルゴリズム を実装する簡単な方法は、マスクをシフトする代わりに、データを所定の位置にシフトすることです。配列は必要ありません。ループ内で_combined >>= 1
_を右シフトして、下位16ビットを比較する方がはるかに簡単です。 (固定マスクを使用するか、_uint16_t
_にキャストします。)
(複数の問題にわたって、マスクの作成は、不要なビットをシフトアウトするよりも効率が悪い傾向があることに気づきました。)
(_uint16_t
_の配列の最後の16ビットチャンク、または特に奇数サイズのバイト配列の最後のバイトを正しく処理することは、読者の練習問題として残されています。)
_// simple brute-force scalar version, checks every bit position 1 at a time.
long bitstream_search_rshift(uint8_t *buf, size_t len, unsigned short pattern)
{
uint16_t *bufshort = (uint16_t*)buf; // maybe unsafe type punning
len /= 2;
for (size_t i = 0 ; i<len-1 ; i++) {
//unsigned short curWord = bufshort[i];
//unsigned short prevWord = bufshort[i+1];
//int combinedWords = (prevWord<<16) + curWord;
uint32_t combined; // assumes little-endian
memcpy(&combined, bufshort+i, sizeof(combined)); // safe unaligned load
for(int bitpos=0; bitpos<16; bitpos++) {
if( (combined&0xFFFF) == pattern) // compiles more efficiently on e.g. old ARM32 without UBFX than (uint16_t)combined
return i*16 + bitpos;
combined >>= 1;
}
}
return -1;
}
_
これは、x86、AArch64、ARMなどのほとんどのISAで、最近のgccとclangを使用して配列からマスクをロードするよりも、大幅にコンパイルされますmore。
コンパイラはループを16で完全に展開するため、即時オペランドを使用してビットフィールド抽出命令を使用できます(ARM ubfx
unsigned bitfield extractまたは PowerPC rwlinm
rotate-left + immediate-など)ビット範囲をマスクする)を使用して、32ビットまたは64ビットレジスタの下部に16ビットを抽出し、通常の比較と分岐を実行できます。実際には、右シフトの依存関係チェーンは1つありません。
X86では、CPUは上位ビットを無視する16ビット比較を実行できます。 combined
のedx
を右シフトした後の_cmp cx,dx
_
一部のISAの一部のコンパイラは、@ Toadのバージョンでこれと同じようにうまく機能します。 PowerPCのclangは、rlwinm
を使用してマスクの配列を最適化し、immediateを使用してcombined
の16ビット範囲をマスクします。16個の事前シフトされたパターン値すべてを16個のレジスタに保持するため、どちらの場合もrlwinm/compare/branchです。 rlwinmの回転カウントがゼロ以外であるかどうか。ただし、右シフトバージョンでは、16個のtmpレジスタを設定する必要はありません。 https://godbolt.org/z/8mUaDI
これを行うには(少なくとも)2つの方法があります。
32ではなく64ビットの要素シフトを使用すると、上位16(ゼロがシフトインされる)を常に無視する代わりに、隣接する複数の16ビットウィンドウをチェックできます。ただし、上位アドレスからの実際のデータではなく、ゼロがシフトインされるSIMD要素の境界でまだ中断があります。 (将来の解決策: VPSHRDW
、SHRD
のSIMDバージョンのようなAVX512VBMI2ダブルシフト。)
とにかくこれを行う価値があるかもしれません。それから、___m256i
_の各64ビット要素の上部で見逃した4x16ビット要素に戻ってきます。たぶん、複数のベクトルにまたがる残り物を組み合わせる。
_// simple brute force, broadcast 32 bits and then search for a 16-bit match at bit offset 0..15
#ifdef __AVX2__
#include <immintrin.h>
long bitstream_search_avx2(uint8_t *buf, size_t len, unsigned short pattern)
{
__m256i vpat = _mm256_set1_epi32(pattern);
len /= 2;
uint16_t *bufshort = (uint16_t*)buf;
for (size_t i = 0 ; i<len-1 ; i++) {
uint32_t combined; // assumes little-endian
memcpy(&combined, bufshort+i, sizeof(combined)); // safe unaligned load
__m256i v = _mm256_set1_epi32(combined);
// __m256i vlo = _mm256_srlv_epi32(v, _mm256_set_epi32(7,6,5,4,3,2,1,0));
// __m256i vhi = _mm256_srli_epi32(vlo, 8);
// shift counts set up to match lane ordering for vpacksswb
// SRLVD cost: Skylake: as fast as other shifts: 1 uop, 2-per-clock
// * Haswell: 3 uops
// * Ryzen: 1 uop, but 3c latency and 2c throughput. Or 4c / 4c for ymm 2 uop version
// * Excavator: latency worse than PSRLD xmm, imm8 by 1c, same throughput. XMM: 3c latency / 1c tput. YMM: 3c latency / 2c tput. (http://users.atw.hu/instlatx64/AuthenticAMD0660F51_K15_BristolRidge_InstLatX64.txt) Agner's numbers are different.
__m256i vlo = _mm256_srlv_epi32(v, _mm256_set_epi32(11,10,9,8, 3,2,1,0));
__m256i vhi = _mm256_srlv_epi32(v, _mm256_set_epi32(15,14,13,12, 7,6,5,4));
__m256i cmplo = _mm256_cmpeq_epi16(vlo, vpat); // low 16 of every 32-bit element = useful
__m256i cmphi = _mm256_cmpeq_epi16(vhi, vpat);
__m256i cmp_packed = _mm256_packs_epi16(cmplo, cmphi); // 8-bit elements, preserves sign bit
unsigned cmpmask = _mm256_movemask_epi8(cmp_packed);
cmpmask &= 0x55555555; // discard odd bits
if (cmpmask) {
return i*16 + __builtin_ctz(cmpmask)/2;
}
}
return -1;
}
#endif
_
これは、特に最初の32バイト未満のデータで、通常はヒットをすばやく見つける検索に適しています。大規模な検索には悪くありませんが(ただし、それでも純粋なブルートフォースであり、一度に1ワードしかチェックしません)、Skylakeでは、複数のウィンドウの16オフセットを並行してチェックするよりも悪くないかもしれません。
これは、変数シフトの効率が低い他のCPUのSkylake向けに調整されており、オフセット0..7に対して1つの変数シフトのみを検討し、それをシフトしてオフセット8..15を作成することができます。または完全に何か他のもの。
これは驚くほどうまくコンパイルされます gcc/clang(on Godbolt) 、メモリから直接ブロードキャストする内部ループを使用します。 (memcpy
の整列されていない負荷とset1()
を単一のvpbroadcastd
に最適化する)
Godboltリンクには、小さな配列で実行するテストmain
も含まれています。 (前回のTweak以降はテストしていない可能性がありますが、以前にテストしたので、パッキング+ビットスキャンのものは機能します。)
_## clang8.0 -O3 -march=skylake inner loop
.LBB0_2: # =>This Inner Loop Header: Depth=1
vpbroadcastd ymm3, dword ptr [rdi + 2*rdx] # broadcast load
vpsrlvd ymm4, ymm3, ymm1
vpsrlvd ymm3, ymm3, ymm2 # shift 2 ways
vpcmpeqw ymm4, ymm4, ymm0
vpcmpeqw ymm3, ymm3, ymm0 # compare those results
vpacksswb ymm3, ymm4, ymm3 # pack to 8-bit elements
vpmovmskb ecx, ymm3 # scalar bitmask
and ecx, 1431655765 # see if any even elements matched
jne .LBB0_4 # break out of the loop on found, going to a tzcnt / ... epilogue
add rdx, 1
add r8, 16 # stupid compiler, calculate this with a multiply on a hit.
cmp rdx, rsi
jb .LBB0_2 # } while(i<len-1);
# fall through to not-found.
_
これは、8uopsの作業+3 uopsのループオーバーヘッドです(and/jneとcmp/jbのマクロ融合を想定しています。これはHaswell/Skylakeで取得します)。 256ビット命令が複数のuopsであるAMDでは、それ以上になります。
またはもちろん、単純な右シフト即時を使用してすべての要素を1シフトし、同じウィンドウ内の複数のオフセットの代わりに複数のウィンドウを並列にチェックします。
効率的な変数シフトがない場合(特にAVX2がまったくない場合)、整理するのにもう少し作業が必要な場合でも、大規模な検索に適していますヒットがあった場合の最初のヒットの場所isヒット。 (最も低い要素以外の場所でヒットを見つけた後、以前のすべてのウィンドウの残りのすべてのオフセットを確認する必要があります。)
大きなビット文字列の一致を見つける簡単な方法は、特定の入力バイトがパターンと一致するビットオフセットを示すルックアップテーブルを計算することです。次に、3つの連続するオフセット一致を組み合わせると、どのオフセットがパターン全体に一致するかを示すビットベクトルを取得できます。たとえば、バイトxがパターンの最初の3ビットに一致し、バイトx + 1がビット3..11に一致し、バイトx + 2がビット11..16に一致する場合、バイトx +5ビットに一致があります。
これを行うサンプルコードを次に示します。一度に2バイトの結果を累積します。
void find_matches(unsigned char* sequence, int n_sequence, unsigned short pattern) {
if (n_sequence < 2)
return; // 0 and 1 byte bitstring can't match a short
// Calculate a lookup table that shows for each byte at what bit offsets
// the pattern could match.
unsigned int match_offsets[256];
for (unsigned int in_byte = 0; in_byte < 256; in_byte++) {
match_offsets[in_byte] = 0xFF;
for (int bit = 0; bit < 24; bit++) {
match_offsets[in_byte] <<= 1;
unsigned int mask = (0xFF0000 >> bit) & 0xFFFF;
unsigned int match_location = (in_byte << 16) >> bit;
match_offsets[in_byte] |= !((match_location ^ pattern) & mask);
}
}
// Go through the input 2 bytes at a time, looking up where they match and
// anding together the matches offsetted by one byte. Each bit offset then
// shows if the input sequence is consistent with the pattern matching at
// that position. This is anded together with the large offsets of the next
// result to get a single match over 3 bytes.
unsigned int curr, next;
curr = 0;
for (int pos = 0; pos < n_sequence-1; pos+=2) {
next = ((match_offsets[sequence[pos]] << 8) | 0xFF) & match_offsets[sequence[pos+1]];
unsigned short match = curr & (next >> 16);
if (match)
output_match(pos, match);
curr = next;
}
// Handle the possible odd byte at the end
if (n_sequence & 1) {
next = (match_offsets[sequence[n_sequence-1]] << 8) | 0xFF;
unsigned short match = curr & (next >> 16);
if (match)
output_match(n_sequence-1, match);
}
}
void output_match(int pos, unsigned short match) {
for (int bit = 15; bit >= 0; bit--) {
if (match & 1) {
printf("Bitstring match at byte %d bit %d\n", (pos-2) + bit/8, bit % 8);
}
match >>= 1;
}
}
このメインループは18命令の長さで、反復ごとに2バイトを処理します。セットアップコストが問題にならない場合、これはそれが得るのとほぼ同じくらい速いはずです。