我编写了函数int compare_16bytes(__m128i lhs, __m128i rhs)
,以便使用SSE指令比较两个16字节数:此函数返回执行比较后相等的字节数。
现在我想使用上面的函数来比较任意长度的两个字节数组:长度可能不是16字节的倍数,所以我需要处理这个问题。我怎样才能完成下面这个功能的实现?我怎样才能改进下面的功能?
int fast_compare(const char* s, const char* t, int length)
{
int result = 0;
const char* sPtr = s;
const char* tPtr = t;
while(...)
{
const __m128i* lhs = (const __m128i*)sPtr;
const __m128i* rhs = (const __m128i*)tPtr;
// compare the next 16 bytes of s and t
result += compare_16bytes(*lhs,*rhs);
sPtr += 16;
tPtr += 16;
}
return result;
}
答案 0 :(得分:5)
正如@Mysticial在上面的评论中所说,做比较并垂直求和,然后在主循环结束时水平求和:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <emmintrin.h>
// reference implementation
int fast_compare_ref(const char *s, const char *t, int length)
{
int result = 0;
int i;
for (i = 0; i < length; ++i)
{
if (s[i] == t[i])
result++;
}
return result;
}
// optimised implementation
int fast_compare(const char *s, const char *t, int length)
{
int result = 0;
int i;
__m128i vsum = _mm_set1_epi32(0);
for (i = 0; i < length - 15; i += 16)
{
__m128i vs, vt, v, vh, vl, vtemp;
vs = _mm_loadu_si128((__m128i *)&s[i]); // load 16 chars from input
vt = _mm_loadu_si128((__m128i *)&t[i]);
v = _mm_cmpeq_epi8(vs, vt); // compare
vh = _mm_unpackhi_epi8(v, v); // unpack compare result into 2 x 8 x 16 bit vectors
vl = _mm_unpacklo_epi8(v, v);
vtemp = _mm_madd_epi16(vh, vh); // accumulate 16 bit vectors into 4 x 32 bit partial sums
vsum = _mm_add_epi32(vsum, vtemp);
vtemp = _mm_madd_epi16(vl, vl);
vsum = _mm_add_epi32(vsum, vtemp);
}
// get sum of 4 x 32 bit partial sums
vsum = _mm_add_epi32(vsum, _mm_srli_si128(vsum, 8));
vsum = _mm_add_epi32(vsum, _mm_srli_si128(vsum, 4));
result = _mm_cvtsi128_si32(vsum);
// handle any residual bytes ( < 16)
if (i < length)
{
result += fast_compare_ref(&s[i], &t[i], length - i);
}
return result;
}
// test harness
int main(void)
{
const int n = 1000000;
char *s = malloc(n);
char *t = malloc(n);
int i, result_ref, result;
srand(time(NULL));
for (i = 0; i < n; ++i)
{
s[i] = rand();
t[i] = rand();
}
result_ref = fast_compare_ref(s, t, n);
result = fast_compare(s, t, n);
printf("result_ref = %d, result = %d\n", result_ref, result);;
return 0;
}
编译并运行上述测试工具:
$ gcc -Wall -O3 -msse3 fast_compare.c -o fast_compare
$ ./fast_compare
result_ref = 3955, result = 3955
$ ./fast_compare
result_ref = 3947, result = 3947
$ ./fast_compare
result_ref = 3945, result = 3945
请注意,在上面的SSE代码中有一个可能非显而易见的技巧,我们使用_mm_madd_epi16
将16位0
/ -1
值解包并累加到32位部分和。我们利用了-1*-1 = 1
(当然还有0*0 = 0
)这一事实 - 我们在这里并没有真正做多次,只需在一条指令中解包和求和。
更新:如下面的评论中所述,这个解决方案并不是最优的 - 我只采用了一个相当优化的16位解决方案,并添加了8位到16位解包,使其适用于8位数据。然而,对于8位数据,存在更有效的方法,例如,使用psadbw
/_mm_sad_epu8
。我将把这个答案留给后人,对于那些可能想要用16位数据做这种事情的人,但实际上其中一个不需要解压缩输入数据的答案应该是接受的答案。 / p>
答案 1 :(得分:3)
在16 x uint8元素中使用部分和可以提供更好的性能 我把循环划分为内循环和外循环 内循环和uint8元素(每个uint8元素总和可以达到255“1”) 小技巧:_mm_cmpeq_epi8将相等的元素设置为0xFF,并且(char)0xFF = -1,这样就可以从总和中减去结果(减去-1以添加1)。
这是我对fast_compare的优化版本:
int fast_compare2(const char *s, const char *t, int length)
{
int result = 0;
int inner_length = length;
int i;
int j = 0;
//Points beginning of 4080 elements block.
const char *s0 = s;
const char *t0 = t;
__m128i vsum = _mm_setzero_si128();
//Outer loop sum result of 4080 sums.
for (i = 0; i < length; i += 4080)
{
__m128i vsum_uint8 = _mm_setzero_si128(); //16 uint8 sum elements (each uint8 element can sum up to 255).
__m128i vh, vl, vhl, vhl_lo, vhl_hi;
//Points beginning of 4080 elements block.
s0 = s + i;
t0 = t + i;
if (i + 4080 <= length)
{
inner_length = 4080;
}
else
{
inner_length = length - i;
}
//Inner loop - sum up to 4080 (compared) results.
//Each uint8 element can sum up to 255. 16 uint8 elements can sum up to 255*16 = 4080 (compared) results.
//////////////////////////////////////////////////////////////////////////
for (j = 0; j < inner_length-15; j += 16)
{
__m128i vs, vt, v;
vs = _mm_loadu_si128((__m128i *)&s0[j]); // load 16 chars from input
vt = _mm_loadu_si128((__m128i *)&t0[j]);
v = _mm_cmpeq_epi8(vs, vt); // compare - set to 0xFF where equal, and 0 otherwise.
//Consider this: (char)0xFF = (-1)
vsum_uint8 = _mm_sub_epi8(vsum_uint8, v); //Subtract the comparison result - subtract (-1) where equal.
}
//////////////////////////////////////////////////////////////////////////
vh = _mm_unpackhi_epi8(vsum_uint8, _mm_setzero_si128()); // unpack result into 2 x 8 x 16 bit vectors
vl = _mm_unpacklo_epi8(vsum_uint8, _mm_setzero_si128());
vhl = _mm_add_epi16(vh, vl); //Sum high and low as uint16 elements.
vhl_hi = _mm_unpackhi_epi16(vhl, _mm_setzero_si128()); //unpack sum of vh an vl into 2 x 4 x 32 bit vectors
vhl_lo = _mm_unpacklo_epi16(vhl, _mm_setzero_si128()); //unpack sum of vh an vl into 2 x 4 x 32 bit vectors
vsum = _mm_add_epi32(vsum, vhl_hi);
vsum = _mm_add_epi32(vsum, vhl_lo);
}
// get sum of 4 x 32 bit partial sums
vsum = _mm_add_epi32(vsum, _mm_srli_si128(vsum, 8));
vsum = _mm_add_epi32(vsum, _mm_srli_si128(vsum, 4));
result = _mm_cvtsi128_si32(vsum);
// handle any residual bytes ( < 16)
if (j < inner_length)
{
result += fast_compare_ref(&s0[j], &t0[j], inner_length - j);
}
return result;
}
答案 2 :(得分:2)
大输入的最快方法是Rotem的答案,其中内部循环为pcmpeqb
/ psubb
,在向量累加器的任何字节元素溢出之前突破到水平和。使用psadbw
对无全局零向量执行无符号字节的hsum。
另请参阅How to count character occurrences using SIMD,您可以在其中使用C ++和AVX2的内在函数来计算匹配,使用从另一个数组加载的向量来代替该问题_mm_set1_epi8(char_to_count)
。有效地加上比较结果是相同的,使用psadbw作为水平和。
如果没有展开/嵌套循环,最好的选择可能是
pcmpeqb -> vector of 0 or 0xFF elements
psadbw -> two 64bit sums of (0*no_matches + 0xFF*matches)
paddq -> accumulate the psadbw result in a vector accumulator
#outside the loop:
horizontal sum
divide the result by 255
如果您的循环中没有很多注册压力,psadbw
针对向量0x7f
而不是全零。
psadbw(0x00, set1(0x7f))
=&gt; sum += 0x7f
psadbw(0xff, set1(0x7f))
=&gt; sum += 0x80
因此,不是除以255(编译器应该在没有实际div
的情况下有效地执行),而是必须减去n * 0x7f
,其中n
是元素的数量。< / p>
另请注意,paddq
在Nehalem和Atom之前速度很慢,因此如果您不希望128 *计数到达,则可以使用paddd
(_mm_add_epi32
)永远溢出32位整数。
这与Paul R的pcmpeqb
/ 2x punpck
/ 2x pmaddwd
/ 2x paddw
相比非常好。
但是通过小的展开,您可以在psadbw / paddq之前累积4或8个比较结果psubb
。
答案 3 :(得分:1)
SSE中的整数比较产生的字节全部为零或全部为1。如果要计数,首先需要右移(不算术)比较结果7,然后添加到结果向量。 最后,您仍然需要通过对其元素求和来减少结果向量。这种减少必须在标量代码中完成,或者使用一系列添加/移位来完成。通常这部分不值得麻烦。