扫描位流中位模式的最快方法

时间:2009-10-15 13:13:48

标签: c++ c algorithm assembly embedded

我需要在比特流中扫描16位字。 不保证在字节或字边界上对齐

实现这一目标的最快方法是什么?有各种蛮力方法;使用表和/或移位,但是有没有“bit twiddling shortcuts”可以通过给出yes / no /也可以包含每个字节或单词到达时的标志结果来减少计算次数?

C代码,内在函数,x86机器代码都很有趣。

13 个答案:

答案 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种安排进行更新。

alt text

因此,左查找表中的索引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


AVX2蛮力

(至少)有两种方法可以做到这一点:

  • 广播单个dword,并使用变量移位在继续操作之前检查其所有位位置。可能非常容易找出您找到比赛的位置。 (如果您要计数所有匹配项,可能会不太好。)
  • 向量加载,并在并行的多个数据窗口的位位置上进行迭代。也许使用从相邻字(16位)开始的未对齐负载执行重叠的奇/偶矢量,以获得双字(32位)窗口。否则,您将不得不跨128位通道进行改组,最好采用16位粒度,并且在没有AVX512的情况下需要2条指令。

使用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个字节。如果设置成本不是问题,那么这应该和它一样快。