计算两个 _m128i SIMD 向量之间的匹配字节数 对于单个向量:

问题描述

我正在开发一个生物信息学工具,我正在尝试使用 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-mpopcntgcc -msse4.2 隐含于任何小于 10 年的事物。 Core 2 缺少硬件 popcnt,但 Nehalem 为 Intel 提供了它。