Как быстро подсчитать бит в отдельные ячейки в серии ints на Sandy Bridge?

Обновление: прочитайте код, это НЕ о подсчете битов в одном int

Можно ли улучшить производительность следующего кода с помощью какого-нибудь умного ассемблера?

uint bit_counter[64];

void Count(uint64 bits) {
  bit_counter[0] += (bits >> 0) & 1;
  bit_counter[1] += (bits >> 1) & 1;
  // ..
  bit_counter[63] += (bits >> 63) & 1;
}

Count находится во внутреннем цикле моего алгоритма.

Update: Архитектура: x86-64, Sandy Bridge, поэтому могут использоваться SSE4.2, AVX1 и более старые технологии, но не AVX2 или BMI1/2.

bits переменная имеет почти случайные биты (близкие к половине нулей и половины)

Ответ 1

Возможно, вы можете сделать 8 сразу, взяв 8 бит на расстоянии 8 друг от друга и сохраняя 8 uint64 для подсчета. Это всего 1 байт на один счетчик, поэтому вы можете накапливать 255 вызовов count, прежде чем вам придется распаковывать эти uint64.

Ответ 2

Вы можете попробовать сделать это с помощью SSE, увеличивая 4 элемента на итерацию.

Предупреждение: непроверенный код следует за...

#include <stdint.h>
#include <emmintrin.h>

uint32_t bit_counter[64] __attribute__ ((aligned(16)));
                     // make sure bit_counter array is 16 byte aligned for SSE

void Count_SSE(uint64 bits)
{
    const __m128i inc_table[16] = {
        _mm_set_epi32(0, 0, 0, 0),
        _mm_set_epi32(0, 0, 0, 1),
        _mm_set_epi32(0, 0, 1, 0),
        _mm_set_epi32(0, 0, 1, 1),
        _mm_set_epi32(0, 1, 0, 0),
        _mm_set_epi32(0, 1, 0, 1),
        _mm_set_epi32(0, 1, 1, 0),
        _mm_set_epi32(0, 1, 1, 1),
        _mm_set_epi32(1, 0, 0, 0),
        _mm_set_epi32(1, 0, 0, 1),
        _mm_set_epi32(1, 0, 1, 0),
        _mm_set_epi32(1, 0, 1, 1),
        _mm_set_epi32(1, 1, 0, 0),
        _mm_set_epi32(1, 1, 0, 1),
        _mm_set_epi32(1, 1, 1, 0),
        _mm_set_epi32(1, 1, 1, 1)
    };

    for (int i = 0; i < 64; i += 4)
    {
        __m128i vbit_counter = _mm_load_si128(&bit_counter[i]);
                                          // load 4 ints from bit_counter
        int index = (bits >> i) & 15;     // get next 4 bits
        __m128i vinc = inc_table[index];  // look up 4 increments from LUT
        vbit_counter = _mm_add_epi32(vbit_counter, vinc);
                                          // increment 4 elements of bit_counter
        _mm_store_si128(&bit_counter[i], vbit_counter);
    }                                     // store 4 updated ints
}

Как это работает: по существу, все, что мы делаем здесь, это векторизация исходного цикла, так что мы обрабатываем 4 бита на итерацию цикла, а не 1. Итак, теперь у нас есть 16 итераций цикла вместо 64. Для каждой итерации мы загружаем 4 бита из bits, затем используйте их как индекс в LUT, который содержит все возможные комбинации из 4 приращений для текущих 4 бит. Затем мы добавляем эти 4 приращения к текущим 4 элементам bit_counter.

Количество загрузок, запасов и добавок уменьшается в 4 раза, но это будет частично компенсировано нагрузкой LUT и другим обслуживанием. Тем не менее, вы все равно можете увидеть 2x-ускорение. Мне было бы интересно узнать результат, если вы решите попробовать его.

Ответ 3

Посмотрите Бит Tweedling Hacks

Изменить Что касается "скопления ковша позиции бит" (bit_counter[]), у меня возникает ощущение, что это может быть хорошим примером для маскировки valarrays+. Тем не менее, это было бы неплохое кодирование + тестирование + профилирование. Дайте мне знать, если вы действительно заинтересованы.

В наши дни вы можете приблизиться к поведению valarray, используя привязанные кортежи (TR1, boost или С++ 11); У меня есть ощущение, что это будет проще читать и медленнее компилировать.

Ответ 4

По-видимому, это можно сделать быстро с помощью "вертикальных счетчиков". На странице текущая страница на битовых трюках (archive) @steike:

Рассмотрим нормальный массив целых чисел, где мы читаем биты по горизонтали:

       msb<-->lsb
  x[0]  00000010  = 2
  x[1]  00000001  = 1
  x[2]  00000101  = 5

Вертикальный счетчик хранит числа, как следует из названия, вертикально; то есть k-бит-счетчик хранится через k слов, с один бит в каждом слове.

  x[0]  00000110   lsb ↑
  x[1]  00000001       |
  x[2]  00000100       |
  x[3]  00000000       |
  x[4]  00000000   msb ↓
             512

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

Мы создаем растровое изображение с 1 бит в положениях, соответствующих счетчики, которые мы хотим увеличить, и перебираем массив из LSB, обновляя бит, когда мы идем. "Перенос" из одного дополнения становится вход для следующего элемента массива.

  input  sum

--------------------------------------------------------------------------------
   A B   C S
   0 0   0 0
   0 1   0 1      sum    = a ^ b
   1 0   0 1      carry  = a & b
   1 1   1 1

  carry = input;
  long *p = buffer;
  while (carry) {
    a = *p; b = carry;
    *p++ = a ^ b;
    carry = a & b;
  }

Для 64-битных слов цикл будет выполняться в среднем 6-7 раз - количество итераций определяется самой длинной цепочкой переносов.

Ответ 5

Вы можете развернуть свою функцию следующим образом. Вероятно, это быстрее, чем может сделать ваш компилятор!

//   rax as 64 bit input
   xor  rcx, rcx                //clear addent

   add  rax, rax                //Copy 63th bit to carry flag
   adc  dword ptr [@bit_counter + 63 * 4], ecx    //Add carry bit to counter[64]

   add  rax, rax                //Copy 62th bit to carry flag
   adc  dword ptr [@bit_counter + 62 * 4], ecx    //Add carry bit to counter[63]

   add  rax, rax                //Copy 62th bit to carry flag
   adc  dword ptr [@bit_counter + 61 * 4], ecx    //Add carry bit to counter[62]
//   ...
   add  rax, rax                //Copy 1th bit to carry flag
   adc  dword ptr [@bit_counter + 1 * 4], ecx     //Add carry bit to counter[1]

   add  rax, rax                //Copy 0th bit to carry flag
   adc  dword ptr [@bit_counter], ecx             //Add carry bit to counter[0]

EDIT:

Вы также можете попробовать с двойным приращением:

//   rax as 64 bit input
   xor  rcx, rcx                //clear addent
//
   add  rax, rax                //Copy 63th bit to carry flag
   rcl  rcx, 33                 //Mov carry to 32th bit as 0bit of second uint
   add  rax, rax                //Copy 62th bit to carry flag
   adc  qword ptr [@bit_counter + 62 * 8], rcx  //Add rcx to 63th and 62th counters

   add  rax, rax                //Copy 61th bit to carry flag
   rcl  rcx, 33                 //Mov carry to 32th bit as 0bit of second uint
   add  rax, rax                //Copy 60th bit to carry flag
   adc  qword ptr [@bit_counter + 60 * 8], rcx  //Add rcx to 61th and 60th counters
//...

Ответ 6

Вы можете использовать набор счетчиков, каждый из которых имеет разный размер. Сначала накапливайте 3 значения в 2-битных счетчиках, затем распаковывайте их и обновляйте 4-битные счетчики. Когда 15 значений готовы, распакуйте до байтов размерами, а после 255 значений обновите бит_counter [].

Вся эта работа может выполняться параллельно в 128-битных SSE-регистрах. На современных процессорах требуется только одна инструкция для распаковки 1 бит в 2. Просто умножьте исходный квадрат на себя с помощью инструкции PCLMULQDQ. Это будет чередовать исходные биты с нулями. Тот же трюк может помочь распаковать 2 бита до 4. И распаковка 4 и 8 бит может быть выполнена с помощью тасований, распаковки и простых логических операций.

Средняя производительность кажется хорошей, но цена составляет 120 байт для дополнительных счетчиков и довольно много ассемблерного кода.

Ответ 7

Нет никакого способа ответить на это вообще; все зависит от компилятора и базовой архитектурой. Единственный реальный способ узнать - попробовать различные решения и меры. (На некоторых машинах, например, сдвиги могут быть очень дорогими. На других нет.) Для начала, я бы использовал что-то вроде:

uint64_t mask = 1;
int index = 0;
while ( mask != 0 ) {
    if ( (bits & mask) != 0 ) {
        ++ bit_counter[index];
    }
    ++ index;
    mask <<= 1;
}

Вскрытие цикла полностью приведет к повышению производительности. В зависимости от архитектуры, заменив if на:

bit_counter[index] += ((bits & mask) != 0);

может быть лучше. Или хуже... невозможно заранее знать. Это также возможно, что на некоторых машинах систематически переходит в бит младшего порядка и маскировка, как вы делаете, были бы лучше.

Некоторые оптимизации также будут зависеть от того, как выглядят типичные данные. Если большинство слов имеют только один или два бита, вы можете получить тестирование байта в момент времени или по четыре бита за раз, и пропускать эти это все нули полностью.

Ответ 8

Если вы считаете, как часто каждый кусок (16 возможностей) возникает при каждом смещении (16 возможностей), вы можете легко суммировать результаты. И эти 256 сумм легко сохраняются:

unsigned long nibble_count[16][16]; // E.g. 0x000700B0 corresponds to [4][7] and [2][B]
unsigned long bitcount[64];

void CountNibbles(uint64 bits) {
  // Count nibbles
  for (int i = 0; i != 16; ++i) {
     nibble_count[i][bits&0xf]++;
     bits >>= 4;
  }
}
void SumNibbles() {
  for (int i = 0; i != 16; ++i) {
    for (int nibble = 0; nibble != 16; ++nibble) {
        for(int bitpos = 0; bitpos != 3; ++bitpos) {
           if (nibble & (1<<bitpos)) {
              bitcount[i*4 + bitpos] += nibble_count[i][nibble];
           }
        }
     }
   }
}

Ответ 9

Это довольно быстро:

void count(uint_fast64_t bits){
    uint_fast64_t i64=ffs64(bits);
    while(i64){
        bit_counter[i64-1]++;
        bits=bits & 0xFFFFFFFFFFFFFFFF << i64;
        i64=ffs64(bits);
    }
}

Вам нужно иметь быструю реализацию ffs для 64 бит. Для большинства компиляторов и процессоров это единственная инструкция. Цикл выполняется один раз для каждого бита в слове, поэтому bits=0 будет очень быстрым, а бит, составляющий 64 бита 1, будет медленнее.

Я тестировал это под 64-битным Ubuntu с GCC, и он производит тот же вывод данных, что и ваш:

void Count(uint64 bits) {
  bit_counter[0] += (bits >> 0) & 1;
  bit_counter[1] += (bits >> 1) & 1;
  // ..
  bit_counter[63] += (bits >> 63) & 1;
}

Скорость изменяется в зависимости от количества бит 1 в 64-битном слове.