C++ 计算两个_m128iSIMD向量之间的匹配字节数

C++ 计算两个_m128iSIMD向量之间的匹配字节数,c++,bioinformatics,sse,simd,hamming-distance,C++,Bioinformatics,Sse,Simd,Hamming Distance,我正在开发一个生物信息学工具,我正在尝试使用SIMD来提高它的速度 给定两个长度为16的字符数组,我需要快速计算字符串匹配的索引数。例如,以下两个字符串“ttttttttttttttttttttttttttttttttttttccc”从第9位到第12位(“TTTT”)匹配,因此输出应为4 如下面的函数foo(工作正常但速度较慢)所示,我将seq1和seq2中的每个字符压缩到u m128i变量s1和s2中,并使用_mm_cmpeq_epi8同时比较每个位置。然后,使用popcnt128(来自Mar

我正在开发一个生物信息学工具,我正在尝试使用SIMD来提高它的速度

给定两个长度为16的字符数组,我需要快速计算字符串匹配的索引数。例如,以下两个字符串“ttttttttttttttttttttttttttttttttttttccc”从第9位到第12位(“TTTT”)匹配,因此输出应为4

如下面的函数foo(工作正常但速度较慢)所示,我将seq1和seq2中的每个字符压缩到u m128i变量s1和s2中,并使用_mm_cmpeq_epi8同时比较每个位置。然后,使用popcnt128(来自Marat Dukhan)将匹配位数相加

float foo(char* seq1, char* seq2) {
    __m128i s1, s2, ceq;
    int match;
    s1 =  _mm_load_si128((__m128i*)(seq1));
    s2 =  _mm_load_si128((__m128i*)(seq2));
    ceq = _mm_cmpeq_epi8(s1, s2);
    match = (popcnt128(ceq)/8);
    return match;
}
尽管Marat Dukhan的popcnt128比简单地将_m128i中的每一位相加要快得多,但_popcnt128()是函数中最慢的瓶颈,占据了大约80%的计算速度。因此,我想提出一种替代popcnt128的方法


我试图将
\uuu m128i ceq
解释为字符串,并将其用作预计算查找表的键,该表将字符串映射到总位数。如果char数组是可散列的,我可以执行如下操作

union{__m128i ceq; char c_arr[16];}
match = table[c_arr] // table = unordered map
如果我尝试对字符串执行类似操作(即,
union{{uuuuu m128i ceq;string s;};
),我会收到以下错误消息:“:()”被隐式删除,因为默认定义的格式可能不正确”。当我尝试其他东西时,我遇到了分割错误


有没有办法让编译器将uu m128i读取为字符串,这样我就可以直接使用u m128i作为无序映射的键?我不明白为什么它不能工作,因为string是一个连续的字符数组,它自然可以用_m128i表示。但我无法让它工作,也无法在线找到任何解决方案。

您可能正在对更长的序列、多个SIMD数据向量执行此操作。在这种情况下,您可以在一个向量中累积计数,只在最后求和单独对每个向量进行popcount的效率要低得多。

参见-而不是
\u mm256\u set1\u epi8(c)
要搜索特定字符,请从其他字符串加载。其他所有操作都相同,包括
计数=_mm_sub_epi8(计数,_mm_cmpeq_epi8(s1,s2))
在内部循环中,循环展开。(比较结果是一个整数0/-1,因此减去它会将0或1添加到另一个向量中。)这在256次迭代后有溢出的风险,最多255次。该链接问题使用AVX2,但这些内部函数的
版本只需要SSE2。(当然,AVX2可以让每个向量指令完成的工作量增加一倍。)

使用
_mm_sad_epu8(v,_mm_setzero_si128())对外部循环中的字节计数器进行水平求和然后累积到另一个计数向量中同样,这都在链接Q&A的代码中,所以只需复制/粘贴它,并将另一个字符串的加载添加到内部循环中,而不是使用广播常量


对于单个向量: 您不需要计算
中的所有位;通过将每个元素的1位提取为标量整数,利用每个字节中的所有8位都相同的事实。(与其他一些SIMD ISA不同,x86 SIMD可以有效地实现这一点)

另一个可能的选项是针对0的
psadbw
(比较结果上的字节数hsum),但这需要最后的hsum步骤qword halves,因此这将比HW popcnt更糟糕。但是如果您不能使用
-mpopnt
进行编译,那么如果您需要仅使用SSE2的基线x86-64,则值得考虑。(您还需要在psadbw之前求反,或将总和缩小1/255…)

(请注意,psadbw策略基本上与我在回答的第一部分中所描述的相同,但只针对单个向量,没有利用将多个计数廉价地添加到一个向量累加器中的能力。)

如果您确实需要将结果作为一个
float
,那么
psadbw
策略就不那么糟糕了:您可以始终将值保存在SIMD向量中,使用
\u mm\u cvtepi32\u ps
对水平和结果进行压缩转换(甚至比
cvtsi2ss
int->float标量转换更便宜)<代码>\u mm\u cvtps\u f32是免费的;标量浮点只是XMM寄存器的低位元素

但是说真的,你现在真的需要一个整数计数作为
浮点数吗?你能不能至少等到你得到所有向量的和,或者保持它为整数


-mpopcnt
gcc-msse4.2
-march=native
在任何小于10岁的物体上暗示。Core 2缺少硬件popcnt,但Nehalem为Intel提供了它。

您可能是在为更长的序列、多个SIMD数据向量执行此操作。在这种情况下,您可以在一个向量中累积计数,只在最后求和单独对每个向量进行popcount的效率要低得多。

参见-而不是
\u mm256\u set1\u epi8(c)
要搜索特定字符,请从其他字符串加载。其他所有操作都相同,包括
计数=_mm_sub_epi8(计数,_mm_cmpeq_epi8(s1,s2))
在内部循环中,循环展开。(比较结果是一个整数0/-1,因此减去它会将0或1添加到另一个向量中。)这在256次迭代后有溢出的风险,最多255次。该链接问题使用AVX2,但这些内部函数的
版本只需要SSE2。(当然,AVX2可以让每个向量指令完成的工作量增加一倍。)

使用
_mm_sad_epu8(v,_mm_setzero_si128())对外部循环中的字节计数器进行水平求和然后累积到另一个计数向量中同样,这都在链接问答的代码中,所以只需复制/粘贴t
    count = __builtin_popcnt(_mm_movemask_epi8(cmp_result));