Как посчитать количество установленных бит в 32-битном целом числе? - PullRequest
811 голосов
/ 20 сентября 2008

8 битов, представляющих число 7, выглядят так:

00000111

Три бита установлены.

Что такое алгоритмы для определения количества установленных бит в 32-разрядном целом числе?

Ответы [ 51 ]

814 голосов
/ 20 сентября 2008

Это известно как ' Вес Хэмминга ', 'popcount' или 'боковое сложение'.

«Лучший» алгоритм действительно зависит от того, на каком процессоре вы находитесь и какова ваша схема использования.

Некоторые процессоры имеют одну встроенную инструкцию для этого, а другие имеют параллельные инструкции, которые действуют на битовые векторы. Параллельные инструкции (например, x86 popcnt на процессорах, где они поддерживаются) почти наверняка будут самыми быстрыми. Некоторые другие архитектуры могут иметь медленную инструкцию, реализованную с помощью микрокодированного цикла, который проверяет бит за цикл ( требуется цитирование ).

Предварительно заполненный метод поиска в таблице может быть очень быстрым, если у вашего ЦП большой кэш и / или вы выполняете много этих инструкций в тесном цикле. Однако он может пострадать из-за «пропуска кэша», когда ЦП должен извлечь часть таблицы из основной памяти.

Если вы знаете, что ваши байты будут в основном 0 или 1, то есть очень эффективные алгоритмы для этих сценариев.

Я считаю, что очень хорошим алгоритмом общего назначения является следующий, известный как «параллельный» или «алгоритм SWAR с переменной точностью». Я выразил это на C-подобном псевдо-языке, вам может потребоваться настроить его для работы с конкретным языком (например, используя uint32_t для C ++ и >>> в Java):

int numberOfSetBits(int i)
{
     // Java: use >>> instead of >>
     // C or C++: use uint32_t
     i = i - ((i >> 1) & 0x55555555);
     i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
     return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
}

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


Этот алгоритм побитового SWAR может распараллеливаться для одновременного выполнения в нескольких векторных элементах, а не в одном целочисленном регистре, для ускорения на процессорах с SIMD, но без используемой команды popcount. (например, код x86-64, который должен запускаться на любом процессоре, а не только на Nehalem или более поздней.)

Однако лучший способ использовать векторные инструкции для popcount - это обычно использовать переменную shuffle для поиска в таблице 4 битов одновременно для каждого байта параллельно. (4 бита индексируют таблицу из 16 записей, содержащуюся в векторном регистре).

В процессорах Intel аппаратная 64-битная команда popcnt может превзойти SSSE3 PSHUFB параллельную битовую реализацию примерно в 2 раза, но только , если ваш компилятор все правильно . В противном случае SSE может выйти значительно вперед. Более новые версии компилятора знают о проблеме ложной зависимости popcnt на Intel .

Ссылки

https://graphics.stanford.edu/~seander/bithacks.html

https://en.wikipedia.org/wiki/Hamming_weight

http://gurmeet.net/puzzles/fast-bit-counting-routines/

http://aggregate.ee.engr.uky.edu/MAGIC/#Population%20Count%20(Ones%20Count)

203 голосов
/ 20 сентября 2008

Также рассмотрите встроенные функции ваших компиляторов.

Например, в компиляторе GNU вы можете просто использовать:

int __builtin_popcount (unsigned int x);
int __builtin_popcountll (unsigned long long x);

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

Встроенные функции GCC работают даже на нескольких платформах. Popcount станет основной в архитектуре x86, так что имеет смысл начать использовать встроенный сейчас. Другие архитектуры имеют популярность в течение многих лет.


В x86 вы можете указать компилятору, что он может предполагать поддержку инструкции popcnt с -mpopcnt или -msse4.2, чтобы также включить векторные инструкции, которые были добавлены в том же поколении. См. Параметры GCC x86 . -march=nehalem (или -march= любой процессор, который вы хотите, чтобы ваш код предполагал и настраивал) мог бы быть хорошим выбором. Запуск полученного двоичного файла на старом процессоре приведет к ошибке недопустимой инструкции.

Чтобы оптимизировать двоичные файлы для машины, на которой вы их собираете, используйте -march=native (с gcc, clang или ICC).

MSVC предоставляет встроенную функцию для инструкции x86 popcnt , но в отличие от gcc это действительно встроенная функция для инструкции по оборудованию и требует аппаратной поддержки.


Использование std::bitset<>::count() вместо встроенного

Теоретически, любой компилятор, который знает, как эффективно выполнять подсчет для целевого ЦП, должен предоставлять эту функциональность через ISO C ++ std::bitset<>. На практике для некоторых целевых процессоров вам может быть выгоднее использовать бит-хак AND / shift / ADD в некоторых случаях.

Для целевых архитектур, где аппаратный popcount является необязательным расширением (например, x86), не все компиляторы имеют std::bitset, который использует его при наличии. Например, MSVC не имеет возможности включить поддержку popcnt во время компиляции и всегда использует просмотр таблицы , даже с /Ox /arch:AVX (что подразумевает SSE4.2, хотя технически есть отдельный бит функции для popcnt.)

Но, по крайней мере, вы получаете что-то переносимое, которое работает везде, а с gcc / clang с правильными целевыми параметрами вы получаете аппаратный подсчет для архитектур, которые его поддерживают.

#include <bitset>
#include <limits>
#include <type_traits>

template<typename T>
//static inline  // static if you want to compile with -mpopcnt in one compilation unit but not others
typename std::enable_if<std::is_integral<T>::value,  unsigned >::type 
popcount(T x)
{
    static_assert(std::numeric_limits<T>::radix == 2, "non-binary type");

    // sizeof(x)*CHAR_BIT
    constexpr int bitwidth = std::numeric_limits<T>::digits + std::numeric_limits<T>::is_signed;
    // std::bitset constructor was only unsigned long before C++11.  Beware if porting to C++03
    static_assert(bitwidth <= std::numeric_limits<unsigned long long>::digits, "arg too wide for std::bitset() constructor");

    typedef typename std::make_unsigned<T>::type UT;        // probably not needed, bitset width chops after sign-extension

    std::bitset<bitwidth> bs( static_cast<UT>(x) );
    return bs.count();
}

См. asm из gcc, clang, icc и MSVC в проводнике компилятора Godbolt.

x86-64 gcc -O3 -std=gnu++11 -mpopcnt испускает это:

unsigned test_short(short a) { return popcount(a); }
    movzx   eax, di      # note zero-extension, not sign-extension
    popcnt  rax, rax
    ret
unsigned test_int(int a) { return popcount(a); }
    mov     eax, edi
    popcnt  rax, rax
    ret
unsigned test_u64(unsigned long long a) { return popcount(a); }
    xor     eax, eax     # gcc avoids false dependencies for Intel CPUs
    popcnt  rax, rdi
    ret

PowerPC64 gcc -O3 -std=gnu++11 испускает (для версии int arg):

    rldicl 3,3,0,32     # zero-extend from 32 to 64-bit
    popcntd 3,3         # popcount
    blr

Этот источник вообще не специфичен для x86 или GNU, но хорошо компилируется только для x86 с помощью gcc / clang / icc.

Также обратите внимание, что запасной вариант gcc для архитектур без единой инструкции popcount - это поиск по байтам за раз. Это не замечательно для ARM, например .

173 голосов
/ 21 сентября 2008

По моему мнению, «лучшее» решение - это то, которое может быть прочитано другим программистом (или оригинальным программистом два года спустя) без обильных комментариев. Возможно, вы захотите самое быстрое или умное решение, которое некоторые уже предоставили, но я предпочитаю удобство чтения в любое время.

unsigned int bitCount (unsigned int value) {
    unsigned int count = 0;
    while (value > 0) {           // until all bits are zero
        if ((value & 1) == 1)     // check lower bit
            count++;
        value >>= 1;              // shift bits, removing lower bit
    }
    return count;
}

Если вам нужна большая скорость (и при условии, что вы хорошо ее документируете, чтобы помочь своим преемникам), вы можете использовать поиск по таблице:

// Lookup table for fast calculation of bits set in 8-bit unsigned char.

static unsigned char oneBitsInUChar[] = {
//  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F (<- n)
//  =====================================================
    0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, // 0n
    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, // 1n
    : : :
    4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, // Fn
};

// Function for fast calculation of bits set in 16-bit unsigned short.

unsigned char oneBitsInUShort (unsigned short x) {
    return oneBitsInUChar [x >>    8]
         + oneBitsInUChar [x &  0xff];
}

// Function for fast calculation of bits set in 32-bit unsigned int.

unsigned char oneBitsInUInt (unsigned int x) {
    return oneBitsInUShort (x >>     16)
         + oneBitsInUShort (x &  0xffff);
}

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

95 голосов
/ 20 сентября 2008

От восторга хакера, с. 66, рисунок 5-2

int pop(unsigned x)
{
    x = x - ((x >> 1) & 0x55555555);
    x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
    x = (x + (x >> 4)) & 0x0F0F0F0F;
    x = x + (x >> 8);
    x = x + (x >> 16);
    return x & 0x0000003F;
}

Выполняется в ~ 20-ти инструкциях (зависит от арки), без ветвления.

Восторг хакера восхитительно! Настоятельно рекомендуется.

73 голосов
/ 12 апреля 2013

Я думаю, что самый быстрый способ - без использования справочных таблиц и popcount - заключается в следующем. Он считает установленные биты всего за 12 операций.

int popcount(int v) {
    v = v - ((v >> 1) & 0x55555555);                // put count of each 2 bits into those 2 bits
    v = (v & 0x33333333) + ((v >> 2) & 0x33333333); // put count of each 4 bits into those 4 bits  
    return c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
}

Это работает, потому что вы можете посчитать общее количество установленных бит, разделив их на две половины, посчитав количество установленных бит в обеих половинах, а затем сложив их. Также известен как Divide and Conquer парадигма. Давайте вдаваться в подробности ..

v = v - ((v >> 1) & 0x55555555); 

Количество битов в двух битах может быть 0b00, 0b01 или 0b10. Давайте попробуем разобраться с этим на двух битах.

 ---------------------------------------------
 |   v    |   (v >> 1) & 0b0101   |  v - x   |
 ---------------------------------------------
   0b00           0b00               0b00   
   0b01           0b00               0b01     
   0b10           0b01               0b01
   0b11           0b01               0b10

Это то, что требовалось: последний столбец показывает количество установленных бит в каждой двухбитной паре. Если двухбитное число равно >= 2 (0b10), то and выдает 0b01, иначе оно выдает 0b00.

v = (v & 0x33333333) + ((v >> 2) & 0x33333333); 

Это утверждение должно быть легко понять. После первой операции у нас есть счетчик установленных битов на каждые два бита, теперь мы суммируем этот счет на каждые 4 бита.

v & 0b00110011         //masks out even two bits
(v >> 2) & 0b00110011  // masks out odd two bits

Затем мы суммируем приведенный выше результат, давая нам общее количество установленных бит в 4 битах. Последнее утверждение самое хитрое.

c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;

Давайте разберемся дальше ...

v + (v >> 4)

Это похоже на второе утверждение; вместо этого мы считаем установленные биты группами по 4. Из-за наших предыдущих операций мы знаем, что в каждом клеве есть количество установленных бит. Давайте посмотрим пример. Предположим, у нас есть байт 0b01000010. Это означает, что у первого полубайта установлены 4 бита, а у второго - 2 бита. Теперь мы добавим эти кусочки вместе.

0b01000010 + 0b01000000

Он дает нам количество установленных бит в байте в первом куске 0b01100010, и поэтому мы маскируем последние четыре байта всех байтов в номере (отбрасывая их).

0b01100010 & 0xF0 = 0b01100000

Теперь каждый байт содержит количество установленных бит. Нам нужно сложить их все вместе. Хитрость заключается в умножении результата на 0b10101010, что имеет интересное свойство. Если наше число имеет четыре байта, A B C D, это приведет к новому числу с этими байтами A+B+C+D B+C+D C+D D. Для 4-байтового номера может быть установлено максимум 32 бита, которые могут быть представлены как 0b00100000.

Все, что нам сейчас нужно, это первый байт, в котором есть сумма всех установленных бит во всех байтах, и мы получаем его >> 24. Этот алгоритм был разработан для 32 bit слов, но его можно легко изменить для 64 bit слов.

54 голосов
/ 20 сентября 2008

Если вы используете Java, встроенный метод Integer.bitCount сделает это.

53 голосов
/ 25 сентября 2008

Мне стало скучно, и я рассчитал миллиард итераций трех подходов. Компилятор gcc -O3. Процессор - это то, что они вставили в MacBook Pro 1-го поколения.

Самое быстрое следующее за 3,7 секунды:

static unsigned char wordbits[65536] = { bitcounts of ints between 0 and 65535 };
static int popcount( unsigned int i )
{
    return( wordbits[i&0xFFFF] + wordbits[i>>16] );
}

Второе место занимает тот же код, но с поиском 4 байта вместо 2 полуслов. Это заняло около 5,5 секунд.

Третье место занимает подход «сложение вбок», который занял 8,6 секунды.

Четвертое место занимает __builtin_popcount () из GCC, за позорные 11 секунд.

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

Так что, если вы заботитесь о производительности превыше всего, используйте первый подход. Если вам не безразлично потратить 64 КБ ОЗУ, используйте второй подход. В противном случае используйте читаемый (но медленный) однобитный подход.

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

Редактировать: Подобные результаты здесь .

30 голосов
/ 05 августа 2012
unsigned int count_bit(unsigned int x)
{
  x = (x & 0x55555555) + ((x >> 1) & 0x55555555);
  x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
  x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F);
  x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF);
  x = (x & 0x0000FFFF) + ((x >> 16)& 0x0000FFFF);
  return x;
}

Позвольте мне объяснить этот алгоритм.

Этот алгоритм основан на алгоритме «разделяй и властвуй». Предположим, что есть 8-битное целое число 213 (11010101 в двоичном виде), алгоритм работает так (каждый раз объединяя два соседних блока):

+-------------------------------+
| 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |  <- x
|  1 0  |  0 1  |  0 1  |  0 1  |  <- first time merge
|    0 0 1 1    |    0 0 1 0    |  <- second time merge
|        0 0 0 0 0 1 0 1        |  <- third time ( answer = 00000101 = 5)
+-------------------------------+
28 голосов
/ 03 октября 2009

Это один из тех вопросов, который помогает узнать вашу микроархитектуру. Я просто рассчитал два варианта в gcc 4.3.3, скомпилированных с -O3, используя встроенные в C ++ значения, чтобы исключить накладные расходы на вызовы функций, один миллиард итераций, сохраняя текущую сумму всех подсчетов, чтобы компилятор не удалил ничего важного, используя rdtsc для синхронизации ( тактовый цикл точный).

inline int pop2(unsigned x, unsigned y)
{
    x = x - ((x >> 1) & 0x55555555);
    y = y - ((y >> 1) & 0x55555555);
    x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
    y = (y & 0x33333333) + ((y >> 2) & 0x33333333);
    x = (x + (x >> 4)) & 0x0F0F0F0F;
    y = (y + (y >> 4)) & 0x0F0F0F0F;
    x = x + (x >> 8);
    y = y + (y >> 8);
    x = x + (x >> 16);
    y = y + (y >> 16);
    return (x+y) & 0x000000FF;
}

Неизменный Восторг Хакера занял 12,2 гигациклов. Моя параллельная версия (считая вдвое больше битов) работает в 13,0 гигациклов. Всего 10,5 с прошло для обоих вместе на 2,4 ГГц Core Duo. 25 гигациклов = чуть более 10 секунд на этой тактовой частоте, поэтому я уверен, что мои настройки правильные.

Это связано с цепочками зависимостей команд, что очень плохо для этого алгоритма. Я мог бы почти удвоить скорость снова, используя пару 64-битных регистров. На самом деле, если бы я был умен и добавил х + у чуть раньше, я мог бы сбрить некоторые смены. 64-битная версия с некоторыми небольшими изменениями получилась бы ровной, но снова посчитала вдвое больше битов.

С 128-разрядными SIMD-регистрами, еще одним фактором, равным двум, и наборами инструкций SSE также часто имеют хитроумные сокращения.

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

ОК, я решил протестировать 64-битную версию. Для этого один размер (без знака long) == 8

inline int pop2(unsigned long x, unsigned long y)
{
    x = x - ((x >> 1) & 0x5555555555555555);
    y = y - ((y >> 1) & 0x5555555555555555);
    x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333);
    y = (y & 0x3333333333333333) + ((y >> 2) & 0x3333333333333333);
    x = (x + (x >> 4)) & 0x0F0F0F0F0F0F0F0F;
    y = (y + (y >> 4)) & 0x0F0F0F0F0F0F0F0F;
    x = x + y; 
    x = x + (x >> 8);
    x = x + (x >> 16);
    x = x + (x >> 32); 
    return x & 0xFF;
}

Это выглядит правильно (хотя я не тщательно проверяю). Теперь время выходит на 10,70 гигациклов / 14,1 гигациклов. Это более позднее число составило 128 миллиардов битов и соответствует 5,9 с, прошедшим на этой машине. Непараллельная версия немного ускоряется, потому что я работаю в 64-битном режиме, и ей нравятся 64-битные регистры немного лучше, чем 32-битные.

Давайте посмотрим, будет ли здесь больше конвейерной обработки OOO. Это было немного сложнее, так что я немного протестировал. Каждый термин в сумме равен 64, а все вместе - 256.

inline int pop4(unsigned long x, unsigned long y, 
                unsigned long u, unsigned long v)
{
  enum { m1 = 0x5555555555555555, 
         m2 = 0x3333333333333333, 
         m3 = 0x0F0F0F0F0F0F0F0F, 
         m4 = 0x000000FF000000FF };

    x = x - ((x >> 1) & m1);
    y = y - ((y >> 1) & m1);
    u = u - ((u >> 1) & m1);
    v = v - ((v >> 1) & m1);
    x = (x & m2) + ((x >> 2) & m2);
    y = (y & m2) + ((y >> 2) & m2);
    u = (u & m2) + ((u >> 2) & m2);
    v = (v & m2) + ((v >> 2) & m2);
    x = x + y; 
    u = u + v; 
    x = (x & m3) + ((x >> 4) & m3);
    u = (u & m3) + ((u >> 4) & m3);
    x = x + u; 
    x = x + (x >> 8);
    x = x + (x >> 16);
    x = x & m4; 
    x = x + (x >> 32);
    return x & 0x000001FF;
}

Я был взволнован на мгновение, но оказалось, что gcc играет встроенные трюки с -O3, хотя я не использую ключевое слово inline в некоторых тестах. Когда я позволяю gcc играть трюки, миллиард вызовов pop4 () занимает 12,56 гигациклов, но я решил, что это сворачивание аргументов в виде константных выражений. Более реалистичное число кажется 19,6gc для еще 30% ускорения. Мой тестовый цикл теперь выглядит следующим образом, убедившись, что каждый аргумент достаточно различен, чтобы gcc не играл трюки.

   hitime b4 = rdtsc(); 
   for (unsigned long i = 10L * 1000*1000*1000; i < 11L * 1000*1000*1000; ++i) 
      sum += pop4 (i,  i^1, ~i, i|1); 
   hitime e4 = rdtsc(); 

256 миллиардов битов за 8,17 секунды. Работает до 1,02 с для 32 миллионов битов, как это было указано в 16-битной таблице поиска. Невозможно сравнить напрямую, потому что другой стенд не дает тактовой частоты, но выглядит так, будто я выплюнул сопли из 64-килобайтного настольного издания, что, во-первых, трагическое использование кэша L1.

Обновление: решил сделать очевидное и создать pop6 (), добавив еще четыре дублированных строки. Вышел на 22,8gc, 384 миллиардов битов, суммированных за 9,5 с. Так что есть еще 20% сейчас при 800 мс для 32 млрд бит.

26 голосов
/ 20 сентября 2008

Почему бы не сделать итеративное деление на 2?

count = 0
while n > 0
  if (n % 2) == 1
    count += 1
  n /= 2  

Я согласен, что это не самое быстрое, но «лучшее» несколько двусмысленно. Я бы сказал, что у «лучшего» должен быть элемент ясности

...