快速计算数组中零值字节的数量

时间:2014-01-04 22:49:37

标签: c++ c bit-manipulation

在大型连续数组中计算零值字节数的快速方法是什么? (或者相反,非零字节的数量。)大,我的意思是2 16 字节或更大。数组的位置和长度可以包含任何字节对齐。

朴素的方式:

int countZeroBytes(byte[] values, int length)
{
    int zeroCount = 0;
    for (int i = 0; i < length; ++i)
        if (!values[i])
            ++zeroCount;

    return zeroCount;
}

对于我的问题,我通常会维护zeroCount并根据values的具体更改进行更新。但是,在发生zeroCount的任意批量更改后,我希望有一种快速,通用的重新计算方法values。我确信有一种更快速完成这项工作的方法,但是,我只是一个新手。

编辑:有些人询问数据的性质是否已经过零检查,因此我将对其进行描述。 (不过,如果解决方案仍然普遍,那就太好了。)

基本上,设想一个由voxels组成的世界(例如Minecraft),将程序生成的地形分隔成立方,或者有效地将内存页面编入三维阵列。每个体素都是飞行加权,作为对应于独特材料(空气,石头,水等)的唯一字节。许多块仅包含空气或水,而其他块包含大量2-4种体素(污垢,沙子等)的不同组合,有效地2-10%的体素是随机异常值。大量存在的体素往往沿着每个轴高度聚集。

但是,似乎零字节计数方法在许多不相关的场景中都很有用。因此,需要一般的解决方案。

6 个答案:

答案 0 :(得分:4)

我已经使用了这个OpenMP实现,它可以利用每个处理器的本地缓存中的数组实际并行读取它。

nzeros_total = 0;
#pragma omp parallel for reduction(+:nzeros_total)
    for (i=0;i<NDATA;i++)
    {
        if (v[i]==0)
            nzeros_total++;
    }

一个快速基准测试,包括运行1000次for循环和一个朴素的实现(与问题中写的OP相同)与OpenMP实现相比,运行1000次,花费两个方法的最佳时间,在QuadCore CPU上使用Windows 7并使用VStudio 2012 Ultimate编译的65536英寸零值元素概率为50%的数组产生以下数字:

               DEBUG               RELEASE
Naive method:  580 microseconds.   341 microseconds.
OpenMP method: 159 microseconds.    99 microseconds.

注意:我已经尝试了#pragma loop (hint_parallel(4))但是很明显,这并没有导致天真版本执行得更好所以我的猜测是编译器已经应用了这个优化,或者它无法应用一点都不此外,#pragma loop (no_vector)并未导致幼稚版本表现更差。

答案 1 :(得分:4)

这将作为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 1/4 SSE寄存器因此分支预测不能很好地工作。但是,1/1024零(0.1%零)更快(参见时间表)。

如果数据长时间存在零,则SIMD会更快。

您可以将16个字节打包到SSE寄存器中。然后,您可以使用_mm_cmpeq_epi8一次性将所有16个字节与零进行比较。然后,为了处理零运行,您可以在结果上使用_mm_movemask_epi8,并且大部分时间它将为零。在这种情况下,你可以获得高达16的加速(前半部分1和下半部分零,我的速度提高了12倍)。

以下是2 ^ 16字节(重复10000次)的秒数表。

                     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>                 // SSE2
#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); //change -1 values to 1
                //hortiontal sum of 16 bytes
                __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;  //1.5% zeros
    //for(int i=0; i<n/2; i++) values[i] = 1;
    //for(int i=n/2; i<n; i++) values[i] = 0;

    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);       
}

答案 2 :(得分:1)

对于0是常见的情况,一次检查64个字节会更快,并且只有在跨度非零时才检查字节。如果零是罕见的,这将更加昂贵。这段代码假设大块可以被64整除。这也假设memcmp和你能得到的效率一样高。

int countZeroBytes(byte[] values, int length)
{
    static const byte zeros[64]={};

    int zeroCount = 0;
    for (int i = 0; i < length; i+=64)
    {
        if (::memcmp(values+i, zeros, 64) == 0)
        {
             zeroCount += 64;
        }
        else
        {
               for (int j=i; j < i+64; ++j)
               {
                     if (!values[j])
                     {
                          ++zeroCount;
                     }
               }
        }
    }

    return zeroCount;
}

答案 3 :(得分:1)

您还可以使用POPCNT指令返回设置的位数。这允许通过消除不必要的分支来进一步简化代码并加速它。以下是AVX2和POPCNT的示例:

position: relative

答案 4 :(得分:0)

避免这种情况可能会更快,并将其换成查找和添加:

char isCharZeroLUT[256] = { 1 }; /* 1 0 0 ... */
int zeroCount = 0;
for (int i = 0; i < length; ++i) {
    zeroCount += isCharZeroLUT[values[i]];
}
但是,我没有测量过这些差异。值得注意的是,一些编译器很乐意向量化足够简单的循环。

答案 5 :(得分:0)

强制计数零字节:使用向量比较指令,如果该字节为0,则将向量的每个字节设置为1,如果该字节不为零,则设置为0。

执行此操作255次以处理最多255 x 64字节(如果您有512位指令可用,或者如果您只有128位向量则执行255 x 32或255 x 16字节)。然后你只需加上255个结果向量。由于比较后的每个字节的值为0或1,因此每个总和最多为255,因此您现在有一个64/32/16字节的向量,低于大约16,000 / 8,000 / 4,000字节。