我需要在位流中扫描一个16位的字。不能保证对齐在字节或字边界上。
实现这个最快的方式是什么?有各种各样的暴力方法;使用表格和/或移位,但是否有任何"位操作快捷方式"可以通过为每个字节或字到达时给出包含标志的yes/no/maybe结果来减少计算次数?
C代码、指令、x86机器码均有意义。
我需要在位流中扫描一个16位的字。不能保证对齐在字节或字边界上。
实现这个最快的方式是什么?有各种各样的暴力方法;使用表格和/或移位,但是否有任何"位操作快捷方式"可以通过为每个字节或字到达时给出包含标志的yes/no/maybe结果来减少计算次数?
C代码、指令、x86机器码均有意义。
有时使用简单的暴力方法是好的。
我认为预先计算单词的所有移位值,并将它们放入16个整数中,这样你就得到了一个类似于以下数组(假设int
比short
宽两倍):
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);
}
然后,对于从流中获取的每个无符号短整型,将该短整型和前一个短整型组成一个整数,并将该无符号整数与16个无符号整数进行比较。如果其中任何一个匹配,则表示找到了一个。
基本上就是这样:
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;
}
&
" 操作和 "==
" 操作,以及分支或其他条件递增操作。此外,还会为每个比特位查找掩码。combined
" 进行右移操作,我们可以获得更高效的汇编代码,如 在另一个答案 中所示,该答案还展示了如何在 x86 上使用 SIMD 进行向量化。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个字节进行比较。下图显示了16位数据在3个字节中的所有可能排列方式,每个字节区域以不同的颜色显示。
alt text http://img70.imageshack.us/img70/8711/80541519.jpg
现在,在第一个样本中将处理1到8的检查,在下一个样本中处理9到16,依此类推。现在,当我们搜索Pattern时,我们将找到此Pattern的所有8种可能的排列方式(如下),并将其存储在3个查找表(左、中和右)中。
初始化查找表:
让我们以0111011101110111
作为要查找的Pattern的示例。现在考虑第4个排列。左侧部分将是XXX01110
。用00010000
填充由左侧部分(XXX01110
)指向的左侧查找表的所有行。 1表示输入Pattern的排列的起始位置。因此,左侧查找表的以下8行将填充为16(00010000
)。
00001110
00101110
01001110
01101110
10001110
10101110
11001110
11101110
11101110
。在中间查找表中通过索引(238)指向的原始指针将被填充为16(00010000
)。111XXXXX
。所有索引为111XXXXX
的原始数据(32个原始数据)将被填充为16(00010000
)。因此,左侧查找表中索引为XX011101
,中间查找表中为11101110
,右侧查找表中为111XXXXX
的原始数据将通过第7次排列更新为00100010
。
搜索模式:
取三个字节的样本。按照以下方式找到计数,其中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 );
}
}
限制:
以上循环无法检测到流缓冲区末尾放置的模式。需要在循环后添加以下代码以克服此限制。
Count = Left[Data[StreamLength - 2]] & Middle[Data[StreamLength - 1]] & 128;
if( Count )
{
TotalCount += GetNumberOfOnes( Count );
}
优点:
该算法只需要N-1个逻辑步骤就能在包含N个字节的数组中找到一个模式。唯一的开销是最初填充查找表,这在所有情况下都是恒定的。因此,对于搜索大量字节流非常有效。
pshufb
。 - Peter Cordes我认为在只有两个字符的情况下,Knuth-Morris-Pratt算法是最好的选择。
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位比较,例如在右移edx
中的combined
之后执行cmp cx,dx
一些ISA的编译器能够像@Toad版本那样很好地处理,例如PowerPC的clang能够优化掉掩码数组,使用rlwinm
使用立即数来屏蔽combined
的16位范围,并将所有16个预移位模式值保留在16个寄存器中,因此无论rlwinm是否具有非零旋转计数,都只是rlwinm / compare / branch。 但右移版本不需要设置16个tmp寄存器。 https://godbolt.org/z/8mUaDI
有(至少)两种方法可以实现:
VPSHRDW
,这是 SHRD
的 SIMD 版本。)也许无论如何都值得这样做,然后回来处理每个 64 位元素顶部遗漏的 4 个 16 位元素,在一个 __m256i
中合并多个向量的剩余部分。// 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
This compiles surprisingly well with gcc/clang (on Godbolt), with an inner loop that broadcasts straight from memory. (Optimizing the memcpy
unaligned load and the set1()
into a single vpbroadcastd
)
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个工作uops加上3个循环开销uops(假设在Haswell/Skylake上使用and/jne和cmp/jb的宏融合)。在AMD上,256位指令需要多个uops,因此会更多。
或者当然可以使用纯右移立即数将所有元素向右移动1位,并且同时检查多个窗口而不是在同一个窗口中使用多个偏移量。
如果没有高效的变量移位(特别是根本没有AVX2),对于大型搜索来说,即使需要更多的工作来确定第一个命中的位置,这也比使用多个偏移量更好。 (在找到除最低元素以外的其他地方的命中后,您需要检查所有较早窗口的所有剩余偏移量。)
atomice的
看起来很好,直到我考虑到Luke和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 );
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。
您可以使用FFT在O(n log n)时间内实现交叉相关。
我认为KMP具有O(n + k)的运行时间,因此它比这个更好。