问题描述
我正在开发一个生物信息学工具,我正在尝试使用 SIMD 来提高它的速度。
给定两个长度为 16 的字符数组,我需要快速计算字符串匹配的索引数量。例如,以下两个字符串“TTTTTTTTTTTTTTTT”和“AAAAGGGGTTTTCCCC”从第 9 到第 12 个位置(“TTTT”)匹配,因此输出应为 4。
如下面的函数 foo(工作正常但速度慢)所示,我将 seq1 和 seq2 中的每个字符打包到 __m128i 变量 s1 和 s2 中,并使用 _mm_cmpeq_epi8 同时比较每个位置。然后,使用 popcnt128(来自 Marat Dukhan 的 Fast counting the number of set bits in __m128i register)将匹配位数相加。
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 的替代方案。
我尝试将 __m128i ceq
解释为字符串,并将其用作预计算查找表的键,该表将字符串映射到总位数。如果 char 数组是可散列的,我可以做类似的事情
union{__m128i ceq; char c_arr[16];}
match = table[c_arr] // table = unordered map
如果我尝试对字符串执行类似的操作(即 union{__m128i ceq; string s;};
),我会收到以下错误消息“::()”被隐式删除,因为默认定义格式错误。当我尝试其他事情时,我遇到了分段错误。
有什么方法可以告诉编译器将 __m128i 作为字符串读取,以便我可以直接使用 __m128i 作为 unordered_map 的键?我不明白为什么它不应该工作,因为字符串是一个连续的字符数组,它可以自然地由 __m128i 表示。但我无法让它工作,也无法在网上找到任何解决方案。
解决方法
您可能正在为更长的序列、多个 SIMD 数据向量执行此操作。在这种情况下,您可以在一个向量中累积计数,而这些计数只能在最后相加。 分别对每个向量进行 popcount 的效率要低得多。
请参阅 How to count character occurrences using SIMD - 而不是 _mm256_set1_epi8(c);
来搜索特定字符,从另一个字符串加载。其他的都一样,包括counts = _mm_sub_epi8(counts,_mm_cmpeq_epi8(s1,s2));
在内循环中,循环展开。 (比较结果是整数 0 / -1,因此减去它会将 0 或 1 添加到另一个向量。)这在 256 次迭代后有溢出的风险,因此最多执行 255 次。该链接问题使用 AVX2,但 {{ 1}} 版本的这些内在函数只需要 SSE2。 (当然,AVX2 会让你每条向量指令完成两倍的工作。)
使用__m128i
对外循环中的字节计数器进行水平求和,然后累加到另一个计数向量中。 同样,这都在链接的问答中的代码中,所以只需复制/粘贴它并将另一个字符串的负载添加到内部循环中,而不是使用广播常量。
对于单个向量:
您不需要计算_mm_sad_epu8(v,_mm_setzero_si128());
中的所有位;通过将每个元素的 1 位提取为标量整数,利用每个字节中的所有 8 位都相同的事实。 (x86 SIMD 可以有效地做到这一点,与其他一些 SIMD ISA 不同)
__m128i
另一个可能选项是 count = __builtin_popcnt(_mm_movemask_epi8(cmp_result));
对 0(比较结果中的字节总和),但这需要 qword 对半的最终 hsum 步骤,因此这将比 HW 更糟糕弹出窗口。但是,如果您无法使用 psadbw
进行编译,那么如果您只需要 SSE2 的基线 x86-64,则值得考虑。 (另外你需要在psadbw之前取反,或者将总和缩小1/255......)
(请注意,psadbw 策略基本上是我在答案的第一部分中描述的内容,但仅针对单个向量,没有利用将多个计数廉价地添加到一个向量累加器中的能力。)
如果您确实需要将结果作为 -mpopcnt
,那么这会使 float
策略不那么糟糕:您可以始终将值保留在 SIMD 向量中,使用 psadbw
来执行水平总和结果的压缩转换(甚至比 _mm_cvtepi32_ps
int->float 标量转换便宜)。 cvtsi2ss
是免费的;标量浮点数只是 XMM 寄存器的低位元素。
但是说真的,你真的需要一个整数作为_mm_cvtps_f32
现在吗?你不能至少等到你有所有的总和向量,还是保持整数?
float
由 -mpopcnt
或 gcc -msse4.2
隐含于任何小于 10 年的事物。 Core 2 缺少硬件 popcnt,但 Nehalem 为 Intel 提供了它。