Чи існує елегантний і швидкий спосіб перевірити 1-біт у цілому чиселі, щоб бути в сусідній області?


85

Мені потрібно перевірити, чи становлення (від 0 до 31 для 32-бітного цілого числа) із бітовим значенням 1 утворюють суміжну область. Наприклад:

00111111000000000000000000000000      is contiguous
00111111000000000000000011000000      is not contiguous

Я хочу, щоб цей тест, тобто якась функція has_contiguous_one_bits(int), була портативною.

Очевидним способом є перебір позицій для пошуку першого встановленого біта, потім першого невстановленого біта та перевірки на наявність більше встановлених бітів.

Цікаво, чи існує швидший спосіб? Якщо є швидкі методи пошуку найвищого та найнижчого встановлених бітів (але з цього питання виявляється, що переносних немає), тоді можлива реалізація

bool has_contiguous_one_bits(int val)
{
    auto h = highest_set_bit(val);
    auto l = lowest_set_bit(val);
    return val == (((1 << (h-l+1))-1)<<l);
}

Для розваги ось перші 100 цілих чисел із суміжними бітами:

0 1 2 3 4 6 7 8 12 14 15 16 24 28 30 31 32 48 56 60 62 63 64 96 112 120 124 126 127 128 192 224 240 248 252 254 255 256 384 448 480 496 504 508 510 511 512 768 896 960 992 1008 1016 1020 1022 1023 1024 1536 1792 1920 1984 2016 2032 2040 2044 2046 2047 2048 3072 3584 3840 3968 4032 4064 4080 4088 4092 4094 4095 4096 6144 7168 7680 7936 8064 8128 8160 8176 8184 8188 8190 8191 8192 12288 14336 15360 15872 16128 16256 16320

вони (звичайно) мають форму (1<<m)*(1<<n-1)з невід’ємними mі n.


4
@aafulei так, 0x0компактний. Легше визначити протилежне (не компактне): якщо є такі біти, то між ними є принаймні один неустановлений біт.
Вальтер

1
@KamilCuk h>=lза (мається на увазі) функціональністю highest_set_bit()таlowest_set_bit()
Walter


6
Це посилання OEIS говорить, що ці цифри не збільшуються, коли вони двійкові. Іншим способом посилатися на них було б сказати, що вони суміжні (або, можливо, пов’язані). Для цього математика "компакт" означає щось зовсім інше.
Teepeemm

1
@Teepeemm Я думаю, що одна з причин, чому це запитання потрапило до гарячих мережевих питань, - саме через неправильне вживання слова compact, це, безумовно, те, чому я натиснув на нього: я не багато думав і гадав, як може мати сенс визначити компактність цей шлях. Очевидно, це не має сенсу.
Ніхто

Відповіді:


147
static _Bool IsCompact(unsigned x)
{
    return (x & x + (x & -x)) == 0;
}

Коротко:

x & -xдає найнижчий біт, встановлений у x(або нуль, якщо xдорівнює нулю).

x + (x & -x) перетворює найнижчий рядок послідовних 1 в одиницю 1 (або обертається в нуль).

x & x + (x & -x) очищає ці 1 біт.

(x & x + (x & -x)) == 0 перевіряє, чи залишились ще 1 біт.

Довше:

-xдорівнює ~x+1, використовуючи доповнення двох, яке ми припускаємо. Після того, як біти перевернуті ~x, додавання 1 переноситься так, що воно перевертає низькі 1 біти ~xі перший 0 біт, але потім зупиняється. Таким чином, низькі біти -xдо першого 1 включно є однаковими з низькими бітами x, але всі старші біти перевертаються. (Приклад: ~10011100дає 01100011, а додавання 1 дає 01100100, отже, низький 100однаковий, але високий 10011перевертається 01100.) Тоді x & -xнам видається єдиний біт, який дорівнює 1 в обох, що є найнижчим 1 бітом ( 00000100). (Якщо xдорівнює нулю, x & -xдорівнює нулю.)

Додавання цього xвикликає перенесення всіх послідовних 1, змінюючи їх на 0. Він залишатиме 1 на наступному вищому 0 біті (або переноситиметься через верхній кінець, залишаючи загальну кількість нуля) ( 10100000.)

Коли це ANDed з x, в місцях, де 1s були змінені на 0, є 0 (а також там, де перенесення змінило 0 на 1). Отже, результат не дорівнює нулю, лише якщо є ще 1 біт вище.


23
Принаймні хтось знає книгу Hacker's Delight. Будь ласка, див. Розділ 2-1 для відповіді. Але на це вже кілька разів давали відповіді тут на SO. У будь-якому разі: +1
Армін Монтіньї

33
Сподіваюся, якщо ви коли-небудь
напишете

14
Це вигідно від x86 BMI1, щоб це зробити x & -xза одну blsiінструкцію, яка становить 1 uop на Intel, 2 up на AMD Zen. godbolt.org/z/5zBx-A . Але без BMI1 версія @ KevinZ ще ефективніша.
Пітер Кордес,

3
@TommyAndersen: _Boolє стандартним ключовим словом, згідно C 2018 6.4.1 1.
Ерік Постпішчіл

1
@Walter: Хм? Цей код використовує unsigned. Якщо ви хочете виконати тест для доповнення двох, підписаний int, найпростіший спосіб - просто передати його в процедуру в цій відповіді, дозволивши intперетворити на unsigned. Це дасть бажаний результат. Застосування операційного шоу до підписаного intбезпосередньо може бути проблематичним через проблеми із переповненням / перенесенням. (Якщо ви хочете перевірити int
чийсь

29

Насправді немає потреби використовувати будь-які внутрішні властивості.

Спочатку переверніть усі 0 перед першим 1. Потім перевірте, чи новим значенням є число Мерсена. У цьому альго нуль відображається як істина.

bool has_compact_bits( unsigned const x )
{
    // fill up the low order zeroes
    unsigned const y = x | ( x - 1 );
    // test if the 1's is one solid block
    return not ( y & ( y + 1 ) );
}

Звичайно, якщо ви хочете використовувати внутрішню інформацію, ось метод popcount:

bool has_compact_bits( unsigned const x )
{
    size_t const num_bits = CHAR_BIT * sizeof(unsigned);
    size_t const sum = __builtin_ctz(x) + __builtin_popcount(x) + __builtin_clz(z);
    return sum == num_bits;
}

2
Перша версія скорочується лише до 4 інструкцій, якщо вона скомпільована -mtbm, використовуючи blsfill/ blcfillінструкції. Це була б найкоротша версія, запропонована на сьогодні. На жаль, майже жоден процесор не підтримує розширення набору інструкцій .
Джованні Черретані

19

Насправді вам не потрібно рахувати провідні нулі. Як пропонується pmg у коментарях, використовуючи той факт, що цифри, які ви шукаєте, є номерами послідовності OEIS A023758 , тобто номери виду 2 ^ i - 2 ^ j з i> = j , ви можете просто порахувати кінцеві нулі ( тобто j - 1 ), переключіть ці біти у вихідне значення (еквівалентно доданню 2 ^ j - 1 ), а потім перевірте, чи має це значення вигляд 2 ^ i - 1 . З внутрішньою ознакою GCC / clang,

bool has_compact_bits(int val) {
    if (val == 0) return true; // __builtin_ctz undefined if argument is zero
    int j = __builtin_ctz(val) + 1;
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

Ця версія трохи швидша, ніж ваша, та запропонована KamilCuk та версія Юрія Фельдмана з лише popcount.

Якщо ви використовуєте C ++ 20, ви можете отримати портативну функцію, замінивши __builtin_ctzна std::countr_zero:

#include <bit>

bool has_compact_bits(int val) {
    int j = std::countr_zero(static_cast<unsigned>(val)) + 1; // ugly cast
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

Акторський склад некрасивий, але попереджає, що при маніпулюванні бітами краще працювати з непідписаними типами. Є альтернативи Pre-C ++ 20 boost::multiprecision::lsb.

Редагувати:

Тест на закреслене посилання був обмежений тим фактом, що для версії Юрія Фельдмана не було видано жодних вказівок. Намагаючись скомпілювати їх на своєму ПК -march=westmere, я виміряв наступний час для 1 мільярда ітерацій з однаковими послідовностями з std::mt19937:

  • ваша версія: 5.7 с
  • Друга версія Камільчука: 4,7 с
  • моя версія: 4.7 с
  • Перша версія Еріка Постпішліла: 4,3 с
  • Версія Юрія Фельдмана (з явним використанням __builtin_popcount): 4,1 с

Отже, принаймні в моїй архітектурі, найшвидшою, здається, є та з popcount.

Редагувати 2:

Я оновив свій орієнтир новою версією Еріка Постпісчіла. Як вимагається в коментарях, код мого тесту можна знайти тут . Я додав цикл заборони, щоб оцінити час, необхідний PRNG. Я також додав дві версії KevinZ. Код складений на clang with -O3 -msse4 -mbmito get popcntта blsiінструкція (завдяки Пітеру Кордесу).

Результати: Принаймні в моїй архітектурі версія Еріка Постпісчіля настільки ж швидка, як і версія Юрія Фельдмана, і принаймні вдвічі швидша, ніж будь-яка інша версія, запропонована до цього часу.


Я видалив операцію: return (x & x + (x & -x)) == 0;.
Eric Postpischil

3
Це порівняльний аналіз старої версії версії @ Eric, чи не так? У поточній версії Ерік компілює gcc -O3 -march=nehalemякнайменше інструкцій з (щоб зробити popcnt доступним) або менше, якщо BMI1 blsiдоступний для x & -x: godbolt.org/z/zuyj_f . І всі інструкції прості, одинарні, за винятком popcntверсії Юрія, яка має 3 латентні затримки. (Але я припускаю, що ви пробували пропускну здатність.) Я також припускаю, що ви, мабуть, вилучили це and valз Юрія, інакше це буде повільніше.
Пітер Кордес,

2
Крім того, яке обладнання ви орієнтували? Пов’язання повного базового коду на Godbolt чи щось інше було б непоганою ідеєю, тому майбутні читачі можуть легко перевірити їх реалізацію на C ++.
Пітер Кордес,

2
Вам також слід протестувати версію @ KevinZ; він компілюється до ще меншої кількості інструкцій без BMI1 (принаймні з clang; невбудована версія gcc марнує a movі не користується перевагами lea): godbolt.org/z/5jeQLQ . З BMI1 версія Еріка все-таки краща на x86-64, принаймні на Intel, де blsiє одинарний uop, але на AMD це 2 коефіцієнти.
Пітер Кордес,

15

Не впевнений у швидкості, але може зробити однокласник, перевіривши, що val^(val>>1)має щонайбільше 2 біти.

Це працює лише з непідписаними типами: необхідне зміщення 0вгорі (логічний зсув), а не арифметичний зсув вправо, який зміщує копію знакового біта.

#include <bitset>
bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2;
}

Щоб відхилити 0(тобто приймати лише вхідні дані, які мають рівно 1 суміжну бітову групу), логічне І з valненульовим значенням. Інші відповіді на це питання сприймаються 0як компактні.

bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2 and val;
}

C ++ портативно виставляє popcount через std::bitset::count(), або в C ++ 20 viastd::popcount . C все ще не має портативного способу, який надійно компілюється у popcnt або подібну інструкцію щодо цілей, де така є.


2
Також найшвидший, поки що.
Джованні Черретані

2
Я думаю, вам потрібно використовувати тип без підпису, щоб переконатися, що ви переносите нулі, а не копії знакового біта. Поміркуйте 11011111. Арифметика зміщена вправо, вона стає 11101111, і XOR є 00110000. За допомогою логічного зрушення вправо (зміщення 0вгорі у верхню частину) ви отримуєте 10110000та правильно визначаєте кілька бітових груп. Редагування, щоб це виправити.
Пітер Кордес,

3
Це справді розумно. Наскільки мені не подобається стиль (IMO просто використовує __builtin_popcount(), кожен компілятор сьогодні має такий примітив), це на сьогоднішній день найшвидший (на сучасному процесорі). Насправді я збираюся стверджувати, що ця презентація серйозно має значення, тому що на процесорі, який не має POPCNT як єдину інструкцію, моя реалізація може перемогти це. Тому, якщо ви збираєтеся використовувати цю реалізацію, вам слід просто використовувати внутрішню. std::bitsetмає жахливий інтерфейс.
KevinZ

9

Процесори мають спеціальні інструкції для цього, дуже швидко. На ПК це BSR / BSF (запроваджено у 80386 р. У 1985 р.), На ARM - CLZ / CTZ

Використовуйте один, щоб знайти індекс найменш значущого встановленого біта, змістіть ціле число вправо на цю величину. Використовуйте інший, щоб знайти індекс найбільш значущого встановленого біта, порівняйте ціле число з (1u << (bsr + 1)) - 1.

На жаль, 35 років було недостатньо для оновлення мови C ++ відповідно до обладнання. Щоб скористатися цими інструкціями з С ++, вам знадобляться внутрішні характеристики, які не є портативними, і повертають результати у дещо інших форматах. Використовуйте препроцесор #ifdefтощо для виявлення компілятора, а потім використовуйте відповідні властивості. У MSVC вони _BitScanForward, _BitScanForward64, _BitScanReverse, _BitScanReverse64. У GCC і clang вони є __builtin_clzі __builtin_ctz.


2
@ e2-e4 Visual studio не підтримує вбудовану збірку під час компіляції для AMD64. Ось чому я рекомендую власні властивості.
Soonts

5
З C ++ 20 існують std::countr_zeroі std::countl_zero. Якщо ви використовуєте Boost, він має портативні обгортки, які називаються boost::multiprecision::lsbі boost::multiprecision::msb.
Джованні Черретані

8
Це взагалі не відповідає на моє запитання - цікаво, чому він отримав будь-які голоси проти
Вальтер

3
@Walter Що ви маєте на увазі “не відповідає”? Я точно відповів, що ви повинні робити, скористайтеся препроцесором, а потім власними характеристиками.
Soonts

2
Очевидно, що C ++ 20 нарешті додає #include <bit> en.cppreference.com/w/cpp/header/bit зі скануванням бітів, popcount та обертанням. Жалюгідно, що так довго тривало портативне викриття бітового сканування, але зараз краще, ніж ніколи. (Портативний popcnt доступний через std::bitset::count().) C ++ 20 все ще не містить деяких речей, які надає Rust ( doc.rust-lang.org/std/primitive.i32.html ), наприклад, зворотний біт та endian, які деякі центральні процесори забезпечують ефективно але не всі. Портативний вбудований модуль для операцій, які мають будь-які центральні процесори, має певний сенс, хоча користувачі повинні знати, що швидко.
Пітер Кордес,

7

Порівняння з нулями замість одиниць збереже деякі операції:

bool has_compact_bits2(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    // Clear bits to the left
    val = (unsigned)val << h;
    int l = __builtin_ctz(val);
    // Invert
    // >>l - Clear bits to the right
    return (~(unsigned)val)>>l == 0;
}

Наведене нижче призводить до одних інструкцій, менших за наведені gcc10 -O3на x86_64 і використовує розширення знаку:

bool has_compact_bits3(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    val <<= h;
    int l = __builtin_ctz(val);
    return ~(val>>l) == 0;
}

Випробувано на болті .


на жаль, це не портативно. Я завжди боюся, що я неправильно сприймаю перевагу оператора з тими операторами змін - ви впевнені ~val<<h>>h>>l == 0, що робите те, що думаєте?
Вальтер

4
Так, я впевнений, у будь-якому випадку відредаговано та додано фігурні дужки. О, отже, вас цікавить портативне рішення? Тому що я подивився there exists a faster way?і припустив, що все йде.
KamilCuk

5

Ви можете переформулювати вимогу:

  • встановіть N кількість бітів, які відрізняються від попередньої (перебираючи біти)
  • якщо N = 2 і і перший або останній біт дорівнює 0, тоді відповідь так
  • якщо N = 1, тоді відповідь так (тому що всі одиниці знаходяться на одній стороні)
  • якщо N = 0 тоді, а будь-який біт дорівнює 0, тоді у вас немає 1, до вас, якщо ви вважаєте, що відповідь так чи ні
  • будь-що інше: відповідь - ні

Проходження всіх бітів може виглядати так:

unsigned int count_bit_changes (uint32_t value) {
  unsigned int bit;
  unsigned int changes = 0;
  uint32_t last_bit = value & 1;
  for (bit = 1; bit < 32; bit++) {
    value = value >> 1;
    if (value & 1 != last_bit  {
      changes++;
      last_bit = value & 1;
    }
  }
  return changes;
}

Але це, безсумнівно, можна оптимізувати (наприклад, перервавши forцикл при valueдосягненні, 0що означає, що більше значущих бітів зі значенням 1 немає).


3

Ви можете виконати таку послідовність обчислень (якщо valприйняти за вхідні дані):

uint32_t x = val;
x |= x >>  1;
x |= x >>  2;
x |= x >>  4;
x |= x >>  8;
x |= x >> 16;

отримати число з усіма нулями нижче найзначнішого, 1заповненого одиницями.

Ви також можете розрахувати, y = val & -valщоб вилучити все, крім найменш значущого 1 біта val(наприклад, 7 & -7 == 1і 12 & -12 == 4).
Попередження: це не вдасться val == INT_MIN, тому вам доведеться розглядати цю справу окремо, але це негайно.

Потім змістіть вправо yна одну позицію, щоб опуститися трохи нижче фактичного LSB val, і виконайте ту ж процедуру, що і для x:

uint32_t y = (val & -val) >> 1;
y |= y >>  1;
y |= y >>  2;
y |= y >>  4;
y |= y >>  8;
y |= y >> 16;

Тоді x - yабо x & ~yабо x ^ yвиробляє "компактну" бітову маску, що охоплює всю довжину val. Просто порівняйте його, щоб valпобачити, чи valє він "компактним".


2

Ми можемо скористатися вбудованими інструкціями gcc, щоб перевірити, чи:

Кількість встановлених бітів

int __builtin_popcount (без підпису int x)
Повертає кількість 1-бітів у x.

дорівнює (a - b):

a : Індекс найвищого встановленого біта (32 - CTZ) (32, оскільки 32 біти в цілому без знака).

int __builtin_clz (непідписаний int x)
Повертає кількість провідних 0-бітів у x, починаючи з найбільш значущої бітової позиції. Якщо x дорівнює 0, результат невизначений.

b : Індекс найнижчого встановленого біта (CLZ):

int __builtin_clz (непідписаний int x)
Повертає кількість провідних 0-бітів у x, починаючи з найбільш значущої бітової позиції. Якщо x дорівнює 0, результат невизначений.

Наприклад, якщо n = 0b0001100110; ми отримаємо 4 із popcount, але різниця індексів (a - b) поверне 6.

bool has_contiguous_one_bits(unsigned n) {
    return (32 - __builtin_clz(n) - __builtin_ctz(n)) == __builtin_popcount(n);
}

що також можна записати як:

bool has_contiguous_one_bits(unsigned n) {
    return (__builtin_popcount(n) + __builtin_clz(n) + __builtin_ctz(n)) == 32;
}

Я не думаю, що це більш елегантно чи ефективно, ніж поточна найбільш прихильна відповідь:

return (x & x + (x & -x)) == 0;

з наступним складанням:

mov     eax, edi
neg     eax
and     eax, edi
add     eax, edi
test    eax, edi
sete    al

але це, мабуть, легше зрозуміти.


1

Гаразд, ось версія, яка перемикається по бітах

template<typename Integer>
inline constexpr bool has_compact_bits(Integer val) noexcept
{
    Integer test = 1;
    while(!(test & val) && test) test<<=1; // skip unset bits to find first set bit
    while( (test & val) && test) test<<=1; // skip set bits to find next unset bit
    while(!(test & val) && test) test<<=1; // skip unset bits to find an offending set bit
    return !test;
}

Перші дві петлі знайшли першу компактну область. Кінцевий цикл перевіряє, чи є якийсь інший біт набору за межами цієї області.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.