Быстрый подсчет количества байтов с нулевым значением в массиве

Что такое быстрый метод подсчета числа нулевого значения байт в большом, смежном массиве? (Или, наоборот, количество ненулевых байтов.) По большому счету, я имею в виду 216 байт или больше. Положение и длина массива могут состоять из любого выравнивания байтов.

наивный образом:

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 происходит. Я уверен, что есть немного странный способ сделать это быстрее, но, увы, я всего лишь новичок.

EDIT: несколько человек спросили о природе данных, проверяемых нулем, поэтому я опишу это. (Было бы неплохо, если бы решения были еще общие.)

в основном, представьте себе мир, состоящий из воксели (например Minecraft), С процедурно генерируемой территорией, разделенной на кубические блоки, или эффективно страницы памяти, индексированные как трехмерные массивы. Каждый воксель взвешивается как уникальный байт, соответствующий уникальному материалу (воздух, камень, вода и т. д.). Многие куски содержат только воздух или воду, в то время как другие содержат различные комбинации 2-4 вокселов в больших количествах (грязь, песок и т. д.), Причем 2-10% вокселов являются случайными выбросами. Существующие воксели в больших количествах, как правило, расположены вдоль каждой оси.

кажется, что метод подсчета нулевых байтов был бы полезен в ряде несвязанных сценариев. Отсюда и стремление к общему решению.

6 ответов


Я пришел с этой реализацией 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 раз тоже, принимая лучшее время для обоих методов, с массивом 65536 ints с нулевой вероятностью элемента значения 50%, используя Windows 7 на процессоре QuadCore и скомпилированный с Vstudio 2012 Ultimate, дает следующие цифры:

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

примечание: Я пробовал #pragma loop (hint_parallel(4)) но, по-видимому, это не заставило наивную версию работать лучше, поэтому я предполагаю, что компилятор уже применял эту оптимизацию, или она не могла быть применена вообще. Кроме того,#pragma loop (no_vector) не заставило наивную версию работать хуже.


это будет идти как O (n), поэтому лучшее, что вы можете сделать, это уменьшить константу. Одно быстрое исправление-удалить ветку. Это дает результат так же быстро, как моя версия SSE ниже, если нули случайным образом отвлекаются. Вероятно, это связано с тем vectorizes ССЗ эту петлю. Однако для длительных пробегов нулей или для случайной плотности нулей менее 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 намного быстрее независимо от плотности.

Edit: на самом деле, это зависит от плотности, просто плотность нулей должна быть меньше, чем я ожидал. 1/64 нуля (1,5% нулей) - это один ноль в 1/4 регистрах SSE, поэтому прогноз ветви работает не очень хорошо. Однако 1/1024 нуля (0,1% нулей) быстрее (см. таблицу умножения).

SIMD еще быстрее, если данные имеют длинные прогоны ноли.

вы можете упаковать 16 байт в регистр SSE. Затем вы можете сравнить все 16 байтов сразу с нулем, используя _mm_cmpeq_epi8. Затем для обработки нулевых пробегов вы можете использовать _mm_movemask_epi8 на результат и большую часть времени она будет равна нулю. В этом случае вы можете получить скорость до 16 (для первой половины 1 и второй половины нуля я получил ускорение 12X).

Вот таблица раз в секундах для 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

вы можете увидеть результаты для последних 1/2 нулей at http://coliru.stacked-crooked.com/a/67a169ddb03d907a

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

для ситуаций, когда 0s являются общими, было бы быстрее проверять 64 байта за раз и проверять только байты, если промежуток не равен нулю. Если zero редки, это будет дороже. Этот код предполагает, что большой блок делится на 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;
}

вы также можете использовать инструкцию POPCNT, которая возвращает количество бит. Это позволяет еще больше упростить код и ускорить его, исключив ненужные ветви. Вот пример с AVX2 и POPCNT:

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include "immintrin.h"

int countZeroes(uint8_t* bytes, int length)
{
    const __m256i vZero = _mm256_setzero_si256();
    int count = 0;
    for (int n = 0; n < length; n += 32)
    {
        __m256i v = _mm256_load_si256((const __m256i*)&bytes[n]);
        v = _mm256_cmpeq_epi8(v, vZero);
        int k = _mm256_movemask_epi8(v);
        count += _mm_popcnt_u32(k);
    }
    return count;
}

#define SIZE 1024

int main()
{
    uint8_t bytes[SIZE] __attribute__((aligned(32)));

    for (int z = 0; z < SIZE; ++z)
        bytes[z] = z % 2;

    int n = countZeroes(bytes, SIZE);
    printf("%d\n", n);

    return 0;
}

это может быть быстрее, чтобы избежать условия и обменять его на поиск и добавить:

char isCharZeroLUT[256] = { 1 }; /* 1 0 0 ... */
int zeroCount = 0;
for (int i = 0; i < length; ++i) {
    zeroCount += isCharZeroLUT[values[i]];
}

Я не измерил различия, хотя. Также стоит отметить, что некоторые компиляторы с удовольствием векторизуют достаточно простые циклы.


грубая сила для подсчета нулевых байтов: используйте инструкцию Vector compare, которая устанавливает каждый байт вектора в 1, если этот байт равен 0, и в 0, если этот байт не равен нулю.

сделайте это 255 раз, чтобы обработать до 255 x 64 байт (если у вас есть 512-битная инструкция, или 255 x 32 или 255 x 16 байт, если у вас есть только 128-битные векторы). А затем вы просто складываете 255 векторов результатов. Поскольку каждый байт после сравнения имел значение 0 или 1, каждая сумма не более 255, поэтому теперь у вас есть один вектор 64 / 32 / 16 байты, вниз от около 16,000 / 8,000 / 4,000 байты.