这是
如何使用SIMD计算字符出现次数的一个特殊情况,其中
c=0
表示要匹配的char(byte)值。请参阅该Q&A以获取手动向量化的AVX2实现
char_count (char const* vector, size_t size, char c);
,其内部循环比此更紧凑,避免将每个0/-1匹配向量分别减少为标量。
这将以O(n)的速度进行,因此您能做的最好的事情是减少常量。一个快速修复方法是删除分支。如果零是随机分布的,则此操作可以提供与下面我的SSE版本一样快的结果。这很可能是因为GCC向量化了这个循环。然而,对于长时间的零运行或零的随机密度小于1%的情况,下面的SSE版本仍然更快。
int countZeroBytes_fix(char* values, int length) {
int zeroCount = 0;
for(int i=0; i<length; i++) {
zeroCount += values[i] == 0;
}
return zeroCount;
}
我最初认为零的密度很重要。至少在使用SSE时,事实并非如此。使用SSE速度更快,与密度无关。
编辑:实际上,它确实取决于密度,只是零的密度必须比我预期的小。 1/64的零(1.5%的零)是1/4 SSE寄存器中的一个零,因此分支预测效果不佳。然而,1/1024的零(0.1%的零)速度更快(请参见时间表)。
如果数据中有长时间的零,则SIMD速度更快。
您可以将16个字节打包到SSE寄存器中。然后,您可以使用_mm_cmpeq_epi8
一次比较所有16个字节是否为零。然后,为了处理零的运行,您可以对结果使用_mm_movemask_epi8
,大多数情况下它将为零。在这种情况下,您可以获得高达16倍的加速(对于第一半为1,第二半为零的情况,我获得了超过12倍的加速)。
以下是10000次重复的2^16字节的时间表(以秒为单位)。
1.5% zeros 50% zeros 0.1% zeros 1st half 1, 2nd half 0
countZeroBytes 0.8s 0.8s 0.8s 0.95s
countZeroBytes_fix 0.16s 0.16s 0.16s 0.16s
countZeroBytes_SSE 0.2s 0.15s 0.10s 0.07s
您可以在http://coliru.stacked-crooked.com/a/67a169ddb03d907a中查看最近1/2个零的结果。
#include <stdio.h>
#include <stdlib.h>
#include <emmintrin.h>
#include <omp.h>
int countZeroBytes(char* values, int length) {
int zeroCount = 0;
for(int i=0; i<length; i++) {
if (!values[i])
++zeroCount;
}
return zeroCount;
}
int countZeroBytes_SSE(char* values, int length) {
int zeroCount = 0;
__m128i zero16 = _mm_set1_epi8(0);
__m128i and16 = _mm_set1_epi8(1);
for(int i=0; i<length; i+=16) {
__m128i values16 = _mm_loadu_si128((__m128i*)&values[i]);
__m128i cmp = _mm_cmpeq_epi8(values16, zero16);
int mask = _mm_movemask_epi8(cmp);
if(mask) {
if(mask == 0xffff) zeroCount += 16;
else {
cmp = _mm_and_si128(and16, cmp);
__m128i sum1 = _mm_sad_epu8(cmp,zero16);
__m128i sum2 = _mm_shuffle_epi32(sum1,2);
__m128i sum3 = _mm_add_epi16(sum1,sum2);
zeroCount += _mm_cvtsi128_si32(sum3);
}
}
}
return zeroCount;
}
int main() {
const int n = 1<<16;
const int repeat = 10000;
char *values = (char*)_mm_malloc(n, 16);
for(int i=0; i<n; i++) values[i] = rand()%64;
int zeroCount = 0;
double dtime;
dtime = omp_get_wtime();
for(int i=0; i<repeat; i++) zeroCount = countZeroBytes(values,n);
dtime = omp_get_wtime() - dtime;
printf("zeroCount %d, time %f\n", zeroCount, dtime);
dtime = omp_get_wtime();
for(int i=0; i<repeat; i++) zeroCount = countZeroBytes_SSE(values,n);
dtime = omp_get_wtime() - dtime;
printf("zeroCount %d, time %f\n", zeroCount, dtime);
}
uint32_t*
迭代而不是天真的uint8_t
)。 - Jarod42