我需要在比特流中扫描16位字。 不保证在字节或字边界上对齐。
实现这一目标的最快方法是什么?有各种蛮力方法;使用表和/或移位,但是有没有“bit twiddling shortcuts”可以通过给出yes / no /也可以包含每个字节或单词到达时的标志结果来减少计算次数?
C代码,内在函数,x86机器代码都很有趣。
答案 0 :(得分:26)
我认为prealc所有移动了单词的值并将它们放入16个整数 所以你有一个像这样的数组
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并将unsigned int与16个unsigned int进行比较。如果他们中的任何一个匹配,你就得到了一个。
基本上就是这样:
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;
}
编辑: 请注意,当在同一位上多次检测到模式时,这可能意味着多次命中:
e.g。 32位0和你要检测的模式是16 0,那么这意味着模式被检测到16次!
编辑: 更正掩码分配
答案 1 :(得分:17)
如果两个字符{0,1}的字母表上的Knuth-Morris-Pratt算法和reinier的想法都不够快,那么这是一个加速搜索速度32倍的技巧。
如果它包含在您要查找的16位字中,则可以首先使用包含256个条目的表来检查位流中的每个字节。你得到的表
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校验和的每个人都知道)。
答案 2 :(得分:10)
我想建议使用3个大小为256的查找表的解决方案。这对于大比特流是有效的。该解决方案在样本中占用3个字节用于比较。下图显示了3字节中16位数据的所有可能排列。每个字节区域都以不同的颜色显示。
alt text http://img70.imageshack.us/img70/8711/80541519.jpg
在第一个样本中将检查1到8,在下一个样本中检查9到16,依此类推。现在,当我们搜索模式时,我们将找到此模式的所有8种可能的安排(如下所示),并将存储在3个查找表中(左,中和对)。
初始化查找表:
让我们以0111011101110111
作为模式进行查找。现在考虑第4种安排。左边部分是XXX01110
。使用XXX01110
填充左侧部分(00010000
)指向左侧查找表的所有原始数据。 1表示输入 Pattern 的排列的起始位置。因此,跟随8个原始左侧查找表将填充16(00010000
)。
00001110
00101110
01001110
01101110
10001110
10101110
11001110
11101110
安排的中间部分是11101110
。原始指向中间查找表中的索引(238)将被填充16(00010000
)。
现在正确的安排部分是111XXXXX
。索引为111XXXXX
的所有原始(32个原始)将填充16(00010000
)。
我们不应该在填充时覆盖查找表中的元素。而是执行按位OR运算来更新已填充的原始数据。在上面的例子中,第3种安排所写的所有原始数据将按照如下第7种安排进行更新。
因此,左查找表中的索引XX011101
和中查找表中的11101110
以及右查找表中的111XXXXX
的原始数据将在第7次排列时更新为00100010
。< / p>
搜索模式:
取三个字节的样本。按以下方式查找 Count ,其中 Left 是左查找表,中是中间查找表,右是右查找表
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;
}
搜索模式:
数据是流缓冲区,左是左查找表,中是中间查找表,右是正确的查找表。
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 字节数组中查找 Pattern 。最初只需要填充查询表,这在所有情况下都是常量。因此,这对于搜索大量字节流非常有效。
答案 3 :(得分:9)
我的钱在Knuth-Morris-Pratt上,字母为两个字符。
答案 4 :(得分:7)
我将实现一个具有16个状态的状态机。
每个状态表示接收的位数符合模式的数量。如果下一个接收的位符合模式的下一位,则机器进入下一个状态。如果不是这种情况,则机器会回退到第一个状态(如果模式的开头可以与较少数量的接收位匹配,则返回另一个状态。)
当机器到达最后一个状态时,这表示已在比特流中识别出模式。
答案 5 :(得分:4)
atomice的
看起来不错,直到我考虑到Luke和MSalter要求提供有关详情的更多信息。
事实证明,细节可能表明比KMP更快捷的方法。 KMP文章链接到
对于搜索模式为“AAAAAA”的特定情况。对于多模式搜索,
可能是最合适的。
您可以找到进一步的介绍性讨论here。
答案 6 :(得分:3)
似乎很好用于SIMD指令。 SSE2添加了一堆整数指令,用于同时处理多个整数,但我无法想象很多解决方案,因为您的数据不会对齐,所以不会涉及很多位移。这实际上听起来像FPGA应该做的事情。
答案 7 :(得分:3)
我要做的是创建16个前缀和16个后缀。然后为每个16位输入块确定最长的后缀匹配。如果下一个块具有长度为(16-N)
后缀匹配实际上不进行16次比较。但是,这需要基于模式字进行预计算。例如,如果模式字是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次检查。
答案 8 :(得分:3)
对于通用的非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 );
答案 9 :(得分:3)
对于极大的输入(n值),可以使用快速傅里叶变换在O(n log n)时间内找到任何位模式。计算位掩码与输入的互相关。分别具有大小n和n'的序列x和掩码y的交叉相关由
定义R(m) = sum _ k = 0 ^ n' x_{k+m} y_k
然后出现与掩码完全匹配的位模式R(m)= Y其中Y是位掩码中的一个的总和。
因此,如果您正在尝试匹配位模式
[0 0 1 0 1 0]
in
[ 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)运行时,所以它打败了它。
答案 10 :(得分:2)
也许您应该在矢量(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++
(希望算法正确)
答案 11 :(得分:2)
一种更简单的实现@Toad's simple brute-force algorithm that checks every bit-position的方法是将数据移到适当位置,而不是移动掩码。不需要任何数组,更简单的是在循环内右移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;
}
对于大多数ISA(例如x86,AArch64和ARM)而言,这比从具有最新gcc和clang的数组中加载掩码要有效得多,而且编译效率更高。
编译器将循环完全展开16,因此他们可以将位域提取指令与立即操作数一起使用(例如ARM ubfx
无符号位域提取或PowerPC rwlinm
左旋转+立即对位范围进行掩码)将16位提取到32或64位寄存器的底部,在这里它们可以进行常规的比较和分支。实际上并没有一个右移1的依存关系链。
在x86上,CPU可以进行16位比较,而忽略高位,例如cmp cx,dx
中的combined
右移后的edx
某些ISA的编译器在使用@Toad的版本时可以很好地完成这项工作,例如用于PowerPC的clang设法优化了掩码数组,使用rlwinm
使用立即数来掩码combined
的16位范围,并将所有16个预移位模式值保留在16个寄存器中,因此方式rlwinm是否具有非零旋转计数只是rlwinm / compare / branch。但是右移版本不需要设置16个tmp寄存器。 https://godbolt.org/z/8mUaDI
(至少)有两种方法可以做到这一点:
使用64位元素移位而不是32位元素移位,我们可以检查多个相邻的16位窗口,而不必始终忽略高16位(移入零)。但是我们仍然在SIMD元素边界上有一个突破,即移入了零,而不是来自更高地址的实际数据。 (未来的解决方案:AVX512VBMI2双班制,例如VPSHRDW
,SIMD版本为SHRD
。)
也许还是值得这样做,然后返回__m256i
中每个64位元素顶部缺少的4个16位元素。也许将多个向量上的剩菜结合起来。
// 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个偏移更糟糕。
这是为Skylake调整的,在其他CPU上,变量移位效率较低,您可能只考虑偏移量0..7的1个变量移位,然后通过偏移量来创建偏移量8..15。或完全其他的东西。
with gcc/clang (on Godbolt)的编译效果令人惊讶,其内部循环直接从内存中广播。 (将memcpy
的未对齐负载和set1()
优化为单个vpbroadcastd
)
Godbolt链接上还包括一个测试main
,它可以在一个小的阵列上运行它。 (自上次调整以来,我可能没有进行过测试,但是我早些时候进行了测试,并且打包+位扫描功能确实起作用。)
## 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.
这是8微克的工作量+ 3微克的循环开销(假设and / jne和cmp / jb的宏融合,我们将在Haswell / Skylake上得到)。在256位指令是多个微指令的AMD上,会更多。
或者当然也可以使用普通的立即右移将所有元素移位1,然后并行检查多个窗口,而不是同一窗口中的多个偏移。
没有高效的可变移位功能(尤其是完全没有AVX2),即使需要更多的工作来整理出第一个匹配项的位置,这对于大型搜索也将是更好的选择有是 (找到最低元素以外的位置之后,您需要检查所有较早窗口的所有剩余偏移量。)
答案 12 :(得分:1)
在大位串中查找匹配的快速方法是计算查找表,该表显示给定输入字节与模式匹配的位偏移。然后将三个连续的偏移匹配组合在一起,您可以得到一个位向量,显示哪些偏移与整个模式匹配。例如,如果字节x匹配模式的前3位,则字节x + 1匹配位3..11,字节x + 2匹配位11..16,则字节x + 5位匹配。
这是执行此操作的一些示例代码,一次累加两个字节的结果:
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个字节。如果设置成本不是问题,那么这应该和它一样快。