8 біт, що представляють число 7, виглядають так:
00000111
Встановлено три біти.
Які алгоритми визначають кількість встановлених бітів у 32-бітовому цілому?
8 біт, що представляють число 7, виглядають так:
00000111
Встановлено три біти.
Які алгоритми визначають кількість встановлених бітів у 32-бітовому цілому?
Відповіді:
Це відомо як " Вага Хеммінга ", "Попконт" або "Додаток убік".
Алгоритм "найкращого" дійсно залежить від того, на якому процесорі ви знаходитесь, і якою є ваша схема використання.
Деякі процесори мають вбудовану інструкцію робити це, а інші мають паралельні інструкції, які діють на бітові вектори. Паралельні інструкції (як x86 popcnt
, на процесорах, де вони підтримуються) майже напевно будуть найшвидшими. Деякі інші архітектури можуть мати повільну інструкцію, реалізовану з мікрокодованим циклом, який тестує біт за цикл ( необхідне цитування ).
Метод пошуку заздалегідь заповненої таблиці може бути дуже швидким, якщо ваш процесор має великий кеш-пам'ять і / або ви виконуєте багато цих інструкцій у вузькому циклі. Однак це може страждати через рахунок "пропуску кешу", де процесор повинен отримати частину таблиці з основної пам'яті. (Знайдіть кожен байт окремо, щоб таблиця була маленькою.)
Якщо ви знаєте, що ваші байти будуть здебільшого 0 або здебільшого 1, то для цих сценаріїв існують дуже ефективні алгоритми.
Я вважаю, що дуже хорошим алгоритмом загального призначення є наступний, відомий як "паралельний" або "алгоритм SWAR змінної точності". Я висловив це псевдомовою, подібною до С, можливо, вам доведеться налаштувати її для роботи для певної мови (наприклад, використовуючи uint32_t для C ++ та >>> на Java):
int numberOfSetBits(uint32_t i)
{
// Java: use int, and 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;
}
Для JavaScript: примушувати до цілого числа з |0
для виконання: зміна першої лініїi = (i|0) - ((i >> 1) & 0x55555555);
Це найкращий варіант поведінки з будь-якого алгоритму, що обговорюється, тому буде ефективно справлятися з будь-якою схемою використання або значеннями, які ви кидаєте на нього.
i = i - ((i >> 1) & 0x55555555);
Перший крок - це оптимізована версія маскування для виділення непарних / парних бітів, переведення їх на лінійку та додавання. Це ефективно робить 16 окремих доповнень у 2-бітових акумуляторах ( SWAR = SIMD у межах реєстру ). Як (i & 0x55555555) + ((i>>1) & 0x55555555)
.
Наступний крок займає непарний / парний вісім з цих 16x 2-бітних акумуляторів і знову додає, виробляючи 8x 4-бітні суми. i - ...
Оптимізація не представляється можливим в цей раз , так це просто маскувати до / після зсуву. Використання однієї 0x33...
і тієї ж константи обох разів замість 0xccc...
перед зміщенням - це добре при компілюванні для ISA, яким потрібно конструювати 32-бітні константи в регістрах окремо.
Заключний крок "зміщення і додавання" (i + (i >> 4)) & 0x0F0F0F0F
розширюється до 4-х 8-бітових акумуляторів. Він маскується після додавання замість попереднього, оскільки максимальне значення в будь-якому 4-бітовому акумуляторі є 4
, якщо всі 4 біти відповідних вхідних бітів були встановлені. 4 + 4 = 8, який все ще вкладається в 4 біти, тому переносити між елементами прикування неможливо i + (i >> 4)
.
Поки що це просто нормальна SIMD з використанням методів SWAR з кількома розумними оптимізаціями. Продовжуючи з тим же малюнком ще 2 кроки, можна розширитись до 2x 16-розрядних, а потім 1-х 32-розрядних. Але є більш ефективний спосіб на машинах із швидким розмноженням апаратних засобів:
Після того, як у нас є досить мало "елементів", множення на магічну константу може підбити всі елементи до верхнього елемента . У цьому випадку елементи байта. Множення відбувається за допомогою зсуву вліво і додавання, тож множення x * 0x01010101
результатів у x + (x<<8) + (x<<16) + (x<<24)
. Наші 8-бітові елементи досить широкі (і містять достатньо малі рахунки), що не призводить до того, що вони входять в перші 8 біт.
64-розрядна версія цього може робити 8x 8-бітні елементи в 64-бітовому цілому з множником 0x0101010101010101 і витягувати високий байт за допомогою >>56
. Тож не потрібно робити зайвих кроків, лише ширші константи. Це те, що GCC використовує для __builtin_popcountll
систем x86, коли popcnt
інструкція з обладнання не включена. Якщо ви можете використовувати для цього вбудовані або внутрішні символи, зробіть це, щоб дати шанс компілятору зробити оптимізацію, орієнтовану на ціль.
Цей бітовий алгоритм SWAR міг би паралелізуватись, що робиться відразу в декількох векторних елементах, а не в єдиному цілому регістрі, для прискорення роботи процесорів із SIMD, але не використовуваною інструкцією з попконтатом. (наприклад, код x86-64, який повинен працювати на будь-якому процесорі, а не лише на Nehalem чи новіших версіях).
Однак найкращий спосіб використовувати векторні вказівки для popcount - це зазвичай за допомогою змінної перетасовки робити пошук таблиці за 4 біти одночасно кожного байту паралельно. (4 біти індексують 16 вхідних таблиць, що знаходяться у векторному регістрі).
На процесорах Intel, інструкція POPCNT апаратного 64bit може випереджати в SSSE3 PSHUFB
біт-паралельна реалізація приблизно в 2 рази, але тільки якщо ваш компілятор отримує це тільки право . Інакше SSE може вийти значно попереду. Новіші версії компілятора відомі з проблемою помилкової залежності popcnt від Intel .
Список літератури:
unsigned int
, щоб легко показати, що воно не містить будь-яких ознак бітових ускладнень. Крім того, було uint32_t
б безпечніше, як в, ви отримуєте те, що очікуєте на всіх платформах?
>>
визначається реалізацією для негативних значень. Аргумент потрібно змінити (або видати) на unsigned
, а оскільки код є 32-бітовим, він, ймовірно, використовує uint32_t
.
Також врахуйте вбудовані функції ваших компіляторів.
Наприклад, у компіляторі 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 забезпечує властивість popcnt
інструкції x86 , але, на відміну від gcc, це справді властива інструкція по апаратному забезпеченню і вимагає апаратної підтримки.
Використання std::bitset<>::count()
замість вбудованого
Теоретично, будь-який компілятор, який знає, як ефективно виконувати ефективність для цільового процесора, повинен відкривати цю функціональність через ISO C ++ std::bitset<>
. На практиці у деяких випадках для деяких цільових процесорів вам може бути краще бит-хак AND / shift / ADD.
Для цільових архітектур, де апаратний попконт є необов'язковим розширенням (наприклад, 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 для архітектур без одноразового попконтату - це пошук таблиці за байтом за часом. Наприклад, це не чудово для ARM .
std::bitset::count
. після вбудовування цього компілюється до одного __builtin_popcount
дзвінка.
На мою думку, "найкращим" рішенням є те, яке може прочитати інший програміст (або оригінальний програміст через два роки) без рясних коментарів. Можливо, ви хочете найшвидшого чи найрозумнішого рішення, яке деякі з них вже запропонували, але я віддаю перевагу читанню над розумністю будь-коли.
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);
}
Хоча вони покладаються на конкретні типи даних, тому вони не є такими портативними. Але оскільки багато оптимізацій продуктивності так і не є портативними, це може не бути проблемою. Якщо ви хочете портативність, я б дотримувався читаного рішення.
if ((value & 1) == 1) { count++; }
з count += value & 1
?
Із захоплення Хекера, с. 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-іш інструкціях (залежно від арки), без розгалуження.
Хакерське захоплення - чудове! Настійно рекомендується.
Integer.bitCount(int)
використовує таку саму точну реалізацію.
pop
замість population_count
(або pop_cnt
якщо у вас повинна бути відмова). @MarcoBolis Я припускаю, що це стосується всіх версій Java, але офіційно це залежатиме від впровадження :)
Я думаю, що найшвидший спосіб - без використання таблиць пошуку та попконтату - наступний. Він рахує встановлені біти всього за 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
. Давайте спробуємо опрацювати це на 2 біти ..
---------------------------------------------
| 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
Це дає нам підрахунок встановлених бітів у байті, в першому nibble, 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
слів.
c =
? Схоже, це слід усунути. Крім того, запропонуйте додатковий батьківський набір A "(((v + (v >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24", щоб уникнути класичних попереджень.
popcount(int v)
і для popcount(unsigned v)
. Для портативності розгляньте popcount(uint32_t v)
тощо. Дійсно як деталь * 0x1010101.
return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
тому нам не потрібно рахувати літери, щоб побачити, що ви насправді робите (оскільки ви відкинули перший 0
, я випадково подумав, що ви використали неправильну (перевернуту) бітову схему як маску - тобто поки я не зазначив, що всього 7 букв, а не 8).
Мені нудно і приурочили мільярд ітерацій трьох підходів. Компілятор - gcc -O3. Процесор - це все, що вони ставлять у MacBook Pro першого покоління.
Найшвидше - це 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 байти замість двох півслов. На це пішло близько 5,5 секунд.
Третє місце посідає бит-подвійний підхід «набік додавання», який зайняв 8,6 секунди.
Четверте місце займає __builtin_popcount () GCC за ганебні 11 секунд.
Підхід підрахунку один раз за часом був повільнішим, і мені нудно було чекати його завершення.
Тож якщо ви переймаєтесь ефективністю понад усе, тоді використовуйте перший підхід. Якщо вам все одно, але недостатньо витратити на це 64 Кб оперативної пам’яті, скористайтеся другим підходом. В іншому випадку використовуйте читаний (але повільний) підхід один раз за часом.
Важко придумати ситуацію, коли ви хочете скористатись підходом біт-подвійності.
Редагувати: Подібні результати тут .
Якщо ви випадково використовуєте Java, вбудований метод Integer.bitCount
зробить це.
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)
+-------------------------------+
Це одне з тих питань, де це допомагає пізнати вашу мікро-архітектуру. Я щойно приуротив два варіанти під gcc 4.3.3, складений з -O3 з використанням рядків C ++ для усунення накладних викликів функцій, один мільярд ітерацій, зберігаючи поточну суму всіх підрахунків, щоб переконатися, що компілятор не видаляє нічого важливого, використовуючи rdtsc для визначення часу ( тактовий цикл точний).
вбудований int pop2 (без знаку x, без підпису 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); повернення (x + y) & 0x000000FF; }
Немодифікований захоплення Хакера взяв 12,2 гігацикла. Моя паралельна версія (рахує вдвічі більше біт) працює в 13,0 гігациклів. 10,5 сек загалом минуло для обох разом на 2,4 ГГц Core Duo. 25 гігациклів = трохи більше 10 секунд на цій тактовій частоті, тож я впевнений, що мої терміни є правильними.
Це пов'язано з ланцюгами залежностей від інструкцій, що дуже погано підходить для цього алгоритму. Я міг майже подвоїти швидкість знову за допомогою пари 64-розрядних регістрів. Насправді, якби я був розумним і додавав х + я трохи раніше, я міг би погладити деякі зміни. 64-розрядна версія з невеликими налаштуваннями вийшла б рівномірною, але знову рахуйте вдвічі більше бітів.
З 128-бітовими регістрами SIMD, ще один фактор два, і набори інструкцій SSE часто мають і розумні скорочення.
Немає причини, щоб код був особливо прозорим. Інтерфейс простий, на алгоритм можна посилатися он-лайн у багатьох місцях, і він піддається всебічному тестуванню одиниць. Програміст, який натрапляє на нього, може навіть чомусь навчитися. Ці бітові операції надзвичайно природні на рівні машини.
Гаразд, я вирішив стендувати 64-бітну версію, яку було налаштовано. Для цього один розмірofof (неподписаний довгий) == 8
вбудований int pop2 (неподписаний довгий х, неподписаний довгий 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)) & 0x0F0F0F0F0F0F0F0F0F; y = (y + (y >> 4)) & 0x0F0F0F0F0F0F0F0F0F; x = x + y; x = x + (x >> 8); x = x + (x >> 16); x = x + (x >> 32); повернути x & 0xFF; }
Це виглядає як правильно (хоча я не перевіряю ретельно). Зараз терміни виходять у 10,70 гігациклів / 14,1 гігациклів. Це число пізніше становило 128 мільярдів біт і відповідає 5,9 секунди, що минула на цій машині. Непаралельна версія прискорює невеликий біт, тому що я працюю в 64-бітному режимі і йому подобаються 64-бітні регістри трохи краще, ніж 32-бітні регістри.
Давайте подивимось, чи буде тут трохи більше трубопроводів ТОВ. Це було трохи більше, тому я фактично трохи перевірив. Кожен окремий термін становить 64, а сумарна сума - 256.
вбудований int pop4 (неподписаний довгий х, неподписаний довгий y, неподписаний довгий u, неподписаний довгий v) { перерахунок {m1 = 0x5555555555555555, м2 = 0x3333333333333333, м3 = 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); повернути x & 0x000001FF; }
Я був на хвильку схвильований, але виявляється, що gcc грає врізні трюки з -O3, хоча я не використовую ключове слово inline в деяких тестах. Коли я дозволяю gcc виконувати трюки, мільярд дзвінків до pop4 () займає 12,56 гігациклів, але я визначив, що це складні аргументи як постійні вирази. Здається, більш реалістичним є 19,6 гс для ще 30% -го збільшення швидкості. Мій тестовий цикл зараз виглядає так, переконуючись, що кожен аргумент є досить різним, щоб зупинити gcc від гри в хитрощі.
hitime b4 = rdtsc (); для (непідписаний довгий i = 10L * 1000 * 1000 * 1000; i <11L * 1000 * 1000 * 1000; ++ i) сума + = pop4 (i, i ^ 1, ~ i, i | 1); hitime e4 = rdtsc ();
Пройшло 256 мільярдів біт, підсумованих за 8,17 секунд. Працює до 1,02 для 32 мільйонів біт, як це відмічено в пошуку 16-бітної таблиці. Неможливо порівняти безпосередньо, тому що інша лава не дає тактової швидкості, але виглядає так, що я вирвав соплі з видання таблиці 64 КБ, що в першу чергу є трагічним використанням кешу L1.
Оновлення: вирішили зробити очевидне і створити pop6 (), додавши ще чотири дублюються рядки. Вийшло 22,8 гс, пройшло 384 мільярди біт за 9,5 с. Отже, є ще 20%, зараз на 800 мс за 32 мільярди біт.
Чому б не ітераційно розділити на 2?
count = 0 при цьому n> 0 якщо (n% 2) == 1 підрахунок + = 1 n / = 2
Я згоден, що це не найшвидше, але "найкраще" є дещо неоднозначним. Я б заперечував, що "найкращий" повинен мати елемент ясності
Біт-подвійне подяка Hacker's Delight стає настільки зрозумілішим, коли ви пишете бітові шаблони.
unsigned int bitCount(unsigned int x)
{
x = ((x >> 1) & 0b01010101010101010101010101010101)
+ (x & 0b01010101010101010101010101010101);
x = ((x >> 2) & 0b00110011001100110011001100110011)
+ (x & 0b00110011001100110011001100110011);
x = ((x >> 4) & 0b00001111000011110000111100001111)
+ (x & 0b00001111000011110000111100001111);
x = ((x >> 8) & 0b00000000111111110000000011111111)
+ (x & 0b00000000111111110000000011111111);
x = ((x >> 16)& 0b00000000000000001111111111111111)
+ (x & 0b00000000000000001111111111111111);
return x;
}
Перший крок додає парні біти до непарних бітів, отримуючи суму бітів у кожному з двох. Інші кроки додають шматки високого порядку до шматок низького порядку, удвічі збільшуючи розмір шматка вгору, поки ми не отримаємо остаточний підрахунок, який займає всю цілу.
Для щасливого середнього між 2 таблицею пошуку 32 та повторенням кожного біта окремо:
int bitcount(unsigned int num){
int count = 0;
static int nibblebits[] =
{0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
for(; num != 0; num >>= 4)
count += nibblebits[num & 0x0f];
return count;
}
Це можна зробити в O(k)
, де k
встановлено кількість бітів.
int NumberOfSetBits(int n)
{
int count = 0;
while (n){
++ count;
n = (n - 1) & n;
}
return count;
}
n &= (n-1)
форму.
Це не найшвидше чи найкраще рішення, але я знайшов те саме питання на своєму шляху, і я почав думати і думати. нарешті я зрозумів, що це можна зробити так, якщо ви отримаєте задачу з математичної сторони і намалюєте графік, то виявите, що це функція, яка має деяку періодичну частину, і тоді ви розумієте різницю між періодами ... так ось ви йдете:
unsigned int f(unsigned int x)
{
switch (x) {
case 0:
return 0;
case 1:
return 1;
case 2:
return 1;
case 3:
return 2;
default:
return f(x/4) + f(x%4);
}
}
def f(i, d={0:lambda:0, 1:lambda:1, 2:lambda:1, 3:lambda:2}): return d.get(i, lambda: f(i//4) + f(i%4))()
Функцію, яку ви шукаєте, часто називають "бічною сумою" або "кількістю населення" двійкового числа. Кнут обговорює це в передфабриці 1A, стор.11-12 (хоча в Томі 2, 4.6.3- (7) було коротке посилання).
Локус Classicus є стаття Пітера Вегнера «Методика для підрахунку одиниць в двійковому комп'ютері», від комунікацій ACM , том 3 (1960) Номер 5, стор 322 . Він надає два різні алгоритми, один оптимізований для чисел, які, як очікується, "розріджені" (тобто мають невелику кількість одиниць), а один для протилежного випадку.
Кілька відкритих питань: -
ми можемо змінити альго, щоб підтримувати від'ємне число наступним чином:
count = 0
while n != 0
if ((n % 2) == 1 || (n % 2) == -1
count += 1
n /= 2
return count
Тепер для подолання другої проблеми ми можемо записати algo на зразок: -
int bit_count(int num)
{
int count=0;
while(num)
{
num=(num)&(num-1);
count++;
}
return count;
}
для повної довідки див .:
http://goursaha.freeoda.com/Miscellaneous/IntegerBitCount.html
Я думаю, що метод Брайана Керніган теж буде корисним ... Він проходить через стільки ітерацій, скільки встановлених біт. Отже, якщо у нас є 32-бітове слово з лише високим набором бітів, воно пройде лише один раз через цикл.
int countSetBits(unsigned int n) {
unsigned int n; // count the number of bits set in n
unsigned int c; // c accumulates the total bits set in n
for (c=0;n>0;n=n&(n-1)) c++;
return c;
}
Опубліковано у 1988 р., Мова програмування С 2-е видання. (Брайан В. Керніган та Денніс М. Річі) згадує це у вправі 2-9. 19 квітня 2006 р. Дон Кнут вказав мені, що цей метод "вперше був опублікований Пітером Вегнером у CACM 3 (1960), 322. (Також його відкрив незалежно Деррік Лемер та опублікував у 1964 р. У книзі за редакцією Бекенбаха.)"
Я використовую наведений нижче код, який є більш інтуїтивним.
int countSetBits(int n) {
return !n ? 0 : 1 + countSetBits(n & (n-1));
}
Логіка: n & (n-1) скидає останній встановлений біт n.
PS: Я знаю, що це не рішення O (1), хоча це цікаве рішення.
O(ONE-BITS)
. Це дійсно O (1), оскільки існує щонайменше 32 однорозрядних.
Що ви розумієте під "Найкращим алгоритмом"? Короткий код або швидкий код? Ваш код виглядає дуже елегантно, і він має постійний час виконання. Код також дуже короткий.
Але якщо швидкість є головним фактором, а не розміром коду, то я думаю, що наступне може бути швидшим:
static final int[] BIT_COUNT = { 0, 1, 1, ... 256 values with a bitsize of a byte ... };
static int bitCountOfByte( int value ){
return BIT_COUNT[ value & 0xFF ];
}
static int bitCountOfInt( int value ){
return bitCountOfByte( value )
+ bitCountOfByte( value >> 8 )
+ bitCountOfByte( value >> 16 )
+ bitCountOfByte( value >> 24 );
}
Я думаю, що це не буде швидше для 64-бітного значення, але 32-бітове значення може бути швидшим.
Я написав швидкий макрос bitcount для машин RISC приблизно в 1990 році. Він не використовує розширену арифметику (множення, поділ,%), витяг пам'яті (занадто повільно), гілки (занадто повільно), але він припускає, що процесор має 32-бітний зсув стовбура (іншими словами, >> 1 і >> 32 займають однакову кількість циклів). Це передбачає, що невеликі константи (наприклад, 6, 12, 24) нічого не коштують для завантаження в регістри, або зберігаються у часописах і повторно використовуватися знову і знову.
За допомогою цих припущень він нараховує 32 біти приблизно за 16 циклів / інструкцій на більшості машин RISC. Зауважте, що 15 інструкцій / циклів близькі до нижньої межі щодо кількості циклів чи інструкцій, оскільки, здається, знадобиться як мінімум 3 інструкції (маска, зсув, оператор), щоб скоротити кількість доповнень навпіл, тому log_2 (32) = 5, 5 x 3 = 15 інструкцій - це квазі-нижня межа.
#define BitCount(X,Y) \
Y = X - ((X >> 1) & 033333333333) - ((X >> 2) & 011111111111); \
Y = ((Y + (Y >> 3)) & 030707070707); \
Y = (Y + (Y >> 6)); \
Y = (Y + (Y >> 12) + (Y >> 24)) & 077;
Ось секрет першого і найскладнішого кроку:
input output
AB CD Note
00 00 = AB
01 01 = AB
10 01 = AB - (A >> 1) & 0x1
11 10 = AB - (A >> 1) & 0x1
тому якщо я беру 1-й стовпчик (А) вище, зміщую його вправо на 1 біт і віднімаю його від AB, я отримую вихід (CD). Розширення до 3 біт аналогічне; Ви можете перевірити це за допомогою 8-рядкової булевої таблиці, як моя вище, якщо бажаєте.
якщо ви використовуєте C ++, ще одним варіантом є використання метапрограмування шаблонів:
// recursive template to sum bits in an int
template <int BITS>
int countBits(int val) {
// return the least significant bit plus the result of calling ourselves with
// .. the shifted value
return (val & 0x1) + countBits<BITS-1>(val >> 1);
}
// template specialisation to terminate the recursion when there's only one bit left
template<>
int countBits<1>(int val) {
return val & 0x1;
}
використання буде:
// to count bits in a byte/char (this returns 8)
countBits<8>( 255 )
// another byte (this returns 7)
countBits<8>( 254 )
// counting bits in a word/short (this returns 1)
countBits<16>( 256 )
можна, звичайно, ще більше розширити цей шаблон, щоб використовувати різні типи (навіть автоматичне виявлення розміру бітів), але я зрозумів, що це просто для ясності.
редагувати: забув згадати це добре, тому що він повинен працювати в будь-якому компіляторі C ++, і він в основному просто розкручує ваш цикл для вас, якщо для підрахунку бітів використовується постійне значення (іншими словами, я впевнений, що це найшвидший загальний метод ти знайдеш)
constexpr
хоч.
Мені особливо подобається цей приклад з файлу fortune:
#define BITCOUNT (x) (((BX_ (x) + (BX_ (x) >> 4)) & 0x0F0F0F0F)% 255) #define BX_ (x) ((x) - ((x) >> 1) & 0x77777777) - (((x) >> 2) та 0x33333333) - (((x) >> 3) & 0x11111111))
Мені це найбільше подобається, тому що він такий гарний!
Java JDK1.5
Integer.bitCount (n);
де n - число, число 1 якого слід підрахувати.
також перевірити,
Integer.highestOneBit(n);
Integer.lowestOneBit(n);
Integer.numberOfLeadingZeros(n);
Integer.numberOfTrailingZeros(n);
//Beginning with the value 1, rotate left 16 times
n = 1;
for (int i = 0; i < 16; i++) {
n = Integer.rotateLeft(n, 1);
System.out.println(n);
}
Я знайшов реалізацію підрахунку бітів у масиві з використанням інструкцій SIMD (SSSE3 та AVX2). Він має в 2-2,5 рази кращу продуктивність, ніж якщо він буде використовувати внутрішню функцію __popcnt64.
Версія SSSE3:
#include <smmintrin.h>
#include <stdint.h>
const __m128i Z = _mm_set1_epi8(0x0);
const __m128i F = _mm_set1_epi8(0xF);
//Vector with pre-calculated bit count:
const __m128i T = _mm_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4);
uint64_t BitCount(const uint8_t * src, size_t size)
{
__m128i _sum = _mm128_setzero_si128();
for (size_t i = 0; i < size; i += 16)
{
//load 16-byte vector
__m128i _src = _mm_loadu_si128((__m128i*)(src + i));
//get low 4 bit for every byte in vector
__m128i lo = _mm_and_si128(_src, F);
//sum precalculated value from T
_sum = _mm_add_epi64(_sum, _mm_sad_epu8(Z, _mm_shuffle_epi8(T, lo)));
//get high 4 bit for every byte in vector
__m128i hi = _mm_and_si128(_mm_srli_epi16(_src, 4), F);
//sum precalculated value from T
_sum = _mm_add_epi64(_sum, _mm_sad_epu8(Z, _mm_shuffle_epi8(T, hi)));
}
uint64_t sum[2];
_mm_storeu_si128((__m128i*)sum, _sum);
return sum[0] + sum[1];
}
Версія AVX2:
#include <immintrin.h>
#include <stdint.h>
const __m256i Z = _mm256_set1_epi8(0x0);
const __m256i F = _mm256_set1_epi8(0xF);
//Vector with pre-calculated bit count:
const __m256i T = _mm256_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4);
uint64_t BitCount(const uint8_t * src, size_t size)
{
__m256i _sum = _mm256_setzero_si256();
for (size_t i = 0; i < size; i += 32)
{
//load 32-byte vector
__m256i _src = _mm256_loadu_si256((__m256i*)(src + i));
//get low 4 bit for every byte in vector
__m256i lo = _mm256_and_si256(_src, F);
//sum precalculated value from T
_sum = _mm256_add_epi64(_sum, _mm256_sad_epu8(Z, _mm256_shuffle_epi8(T, lo)));
//get high 4 bit for every byte in vector
__m256i hi = _mm256_and_si256(_mm256_srli_epi16(_src, 4), F);
//sum precalculated value from T
_sum = _mm256_add_epi64(_sum, _mm256_sad_epu8(Z, _mm256_shuffle_epi8(T, hi)));
}
uint64_t sum[4];
_mm256_storeu_si256((__m256i*)sum, _sum);
return sum[0] + sum[1] + sum[2] + sum[3];
}
Існує багато алгоритму підрахунку встановлених бітів; але я думаю, що найкращий - швидший! На цій сторінці ви можете ознайомитись детально:
Я пропоную це:
Підрахунок бітів, встановлених у 14, 24 або 32-бітових словах, використовуючи 64-бітні інструкції
unsigned int v; // count the number of bits set in v
unsigned int c; // c accumulates the total bits set in v
// option 1, for at most 14-bit values in v:
c = (v * 0x200040008001ULL & 0x111111111111111ULL) % 0xf;
// option 2, for at most 24-bit values in v:
c = ((v & 0xfff) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f;
c += (((v & 0xfff000) >> 12) * 0x1001001001001ULL & 0x84210842108421ULL)
% 0x1f;
// option 3, for at most 32-bit values in v:
c = ((v & 0xfff) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f;
c += (((v & 0xfff000) >> 12) * 0x1001001001001ULL & 0x84210842108421ULL) %
0x1f;
c += ((v >> 24) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f;
Цей метод вимагає ефективності 64-бітного процесора з швидким поділом модуля. Перший варіант займає лише 3 операції; другий варіант займає 10; а третій варіант займає 15.
Швидке рішення C # за допомогою попередньо обчисленої таблиці підрахунків бітів з розгалуженням на вхідний розмір.
public static class BitCount
{
public static uint GetSetBitsCount(uint n)
{
var counts = BYTE_BIT_COUNTS;
return n <= 0xff ? counts[n]
: n <= 0xffff ? counts[n & 0xff] + counts[n >> 8]
: n <= 0xffffff ? counts[n & 0xff] + counts[(n >> 8) & 0xff] + counts[(n >> 16) & 0xff]
: counts[n & 0xff] + counts[(n >> 8) & 0xff] + counts[(n >> 16) & 0xff] + counts[(n >> 24) & 0xff];
}
public static readonly uint[] BYTE_BIT_COUNTS =
{
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8
};
}
(0xe994 >>(k*2))&3
, без доступу до пам'яті ...
Ось портативний модуль (ANSI-C), який може орієнтувати кожен ваш алгоритм у будь-якій архітектурі.
У вашому процесорі є 9 бітних байтів? Немає проблем :-) На даний момент він реалізує 2 алгоритми, алгоритм K&R та таблицю пошуку мудрених байтів. Таблиця пошуку в середньому в 3 рази швидша, ніж алгоритм K&R. Якщо хтось може зрозуміти спосіб зробити портативний алгоритм "Хакерське захоплення", сміливо додайте його.
#ifndef _BITCOUNT_H_
#define _BITCOUNT_H_
/* Return the Hamming Wieght of val, i.e. the number of 'on' bits. */
int bitcount( unsigned int );
/* List of available bitcount algorithms.
* onTheFly: Calculate the bitcount on demand.
*
* lookupTalbe: Uses a small lookup table to determine the bitcount. This
* method is on average 3 times as fast as onTheFly, but incurs a small
* upfront cost to initialize the lookup table on the first call.
*
* strategyCount is just a placeholder.
*/
enum strategy { onTheFly, lookupTable, strategyCount };
/* String represenations of the algorithm names */
extern const char *strategyNames[];
/* Choose which bitcount algorithm to use. */
void setStrategy( enum strategy );
#endif
.
#include <limits.h>
#include "bitcount.h"
/* The number of entries needed in the table is equal to the number of unique
* values a char can represent which is always UCHAR_MAX + 1*/
static unsigned char _bitCountTable[UCHAR_MAX + 1];
static unsigned int _lookupTableInitialized = 0;
static int _defaultBitCount( unsigned int val ) {
int count;
/* Starting with:
* 1100 - 1 == 1011, 1100 & 1011 == 1000
* 1000 - 1 == 0111, 1000 & 0111 == 0000
*/
for ( count = 0; val; ++count )
val &= val - 1;
return count;
}
/* Looks up each byte of the integer in a lookup table.
*
* The first time the function is called it initializes the lookup table.
*/
static int _tableBitCount( unsigned int val ) {
int bCount = 0;
if ( !_lookupTableInitialized ) {
unsigned int i;
for ( i = 0; i != UCHAR_MAX + 1; ++i )
_bitCountTable[i] =
( unsigned char )_defaultBitCount( i );
_lookupTableInitialized = 1;
}
for ( ; val; val >>= CHAR_BIT )
bCount += _bitCountTable[val & UCHAR_MAX];
return bCount;
}
static int ( *_bitcount ) ( unsigned int ) = _defaultBitCount;
const char *strategyNames[] = { "onTheFly", "lookupTable" };
void setStrategy( enum strategy s ) {
switch ( s ) {
case onTheFly:
_bitcount = _defaultBitCount;
break;
case lookupTable:
_bitcount = _tableBitCount;
break;
case strategyCount:
break;
}
}
/* Just a forwarding function which will call whichever version of the
* algorithm has been selected by the client
*/
int bitcount( unsigned int val ) {
return _bitcount( val );
}
#ifdef _BITCOUNT_EXE_
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/* Use the same sequence of pseudo random numbers to benmark each Hamming
* Weight algorithm.
*/
void benchmark( int reps ) {
clock_t start, stop;
int i, j;
static const int iterations = 1000000;
for ( j = 0; j != strategyCount; ++j ) {
setStrategy( j );
srand( 257 );
start = clock( );
for ( i = 0; i != reps * iterations; ++i )
bitcount( rand( ) );
stop = clock( );
printf
( "\n\t%d psudoe-random integers using %s: %f seconds\n\n",
reps * iterations, strategyNames[j],
( double )( stop - start ) / CLOCKS_PER_SEC );
}
}
int main( void ) {
int option;
while ( 1 ) {
printf( "Menu Options\n"
"\t1.\tPrint the Hamming Weight of an Integer\n"
"\t2.\tBenchmark Hamming Weight implementations\n"
"\t3.\tExit ( or cntl-d )\n\n\t" );
if ( scanf( "%d", &option ) == EOF )
break;
switch ( option ) {
case 1:
printf( "Please enter the integer: " );
if ( scanf( "%d", &option ) != EOF )
printf
( "The Hamming Weight of %d ( 0x%X ) is %d\n\n",
option, option, bitcount( option ) );
break;
case 2:
printf
( "Please select number of reps ( in millions ): " );
if ( scanf( "%d", &option ) != EOF )
benchmark( option );
break;
case 3:
goto EXIT;
break;
default:
printf( "Invalid option\n" );
}
}
EXIT:
printf( "\n" );
return 0;
}
#endif
що ти можеш зробити
while(n){
n=n&(n-1);
count++;
}
логіка, що стоїть за цим, біти n-1 перевернута з правого встановленого біта n. якщо n = 6, тобто 110, то 5 - 101, біти перевернуті з кращого правого біта n. тож якщо ми та ці два ми зробимо кращий правий біт 0 у кожній ітерації та завжди перейдемо до наступного правого встановленого біта. Отже, підраховуємо встановлений біт. Найгірша складність у часі буде O (logn), коли встановлюється кожен біт.