Есть ли элегантный и быстрый способ проверить, что 1-бит целого числа находится в непрерывной области?

84

Мне нужно проверить, образуют ли позиции (от 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()
Уолтер
6
OEIS A023758
pmg
6
Эта ссылка OEIS говорит, что эти числа не увеличиваются в двоичном формате. Другой способ сослаться на них - сказать, что они смежны (или, возможно, связаны). Для этого математика «компактность» означает совсем другое.
Teepeemm
1
@Teepeemm Я думаю, что одна из причин, по которой этот вопрос попал в горячие сетевые вопросы, заключается именно в неправильном использовании слова компактный, именно поэтому я щелкнул по нему: я особо не думал и задавался вопросом, как это может иметь смысл определять компактность сюда. Очевидно, в этом нет смысла.
Nobody

Ответы:

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

Вкратце:

x & -xдает самый низкий установленный бит x(или ноль, если он xравен нулю).

x + (x & -x) преобразует самую низкую строку последовательных единиц в одну (или оборачивается до нуля).

x & x + (x & -x) очищает эти 1 бит.

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

Дольше:

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

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

Когда это связано с оператором AND x, в местах, где единицы были изменены на 0 (а также в тех местах, где перенос изменил 0 на 1), есть 0. Таким образом, результат не равен нулю, только если есть еще 1 бит выше.

Эрик Постпищил
источник
23
По крайней мере, кто-то знает книгу «Восторг хакера». Пожалуйста, смотрите главу 2-1 для ответа. Но на этот вопрос уже несколько раз ответили здесь, на SO. В любом случае: +1
Армин Монтиньи
33
Я надеюсь, что если вы когда-нибудь напишете такой код в продакшене, вы
включите
14
Это хорошо от того, что x86 BMI1 можно выполнять x & -xв одной blsiинструкции, что составляет 1 мкоп для Intel и 2 мкоп для AMD Zen. godbolt.org/z/5zBx-A . Но без BMI1 версия @ KevinZ еще более эффективна.
Питер Кордес
3
@TommyAndersen: _Boolстандартное ключевое слово, согласно C 2018 6.4.1 1.
Эрик Постпищил
1
@Walter: Хм? Этот код использует unsigned. Если вы хотите выполнить тест для подписанного дополнения до двух int, самый простой способ - просто передать его подпрограмме в этом ответе, позволяя intпреобразовать в unsigned. Это даст желаемый результат. Применение операций show к подписанному intнапрямую может быть проблематичным из-за проблем с переполнением / переносом. (Если вы хотите проверить свое дополнение или знак и величину int, это другой вопрос, в настоящее время представляющий в основном только теоретический интерес.)
Эрик Постпишил
29

На самом деле нет необходимости использовать какие-либо встроенные функции.

Сначала переверните все нули перед первым 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;
}
KevinZ
источник
2
Первая версия сокращается до 4 инструкций, если она скомпилирована с -mtbmиспользованием blsfill/ blcfillинструкциями. Это будет самый короткий вариант из предложенных на данный момент. К сожалению, это расширение набора команд почти не поддерживает процессор .
Джованни Черретани
18

На самом деле вам не нужно считать ведущие нули. Как предлагает 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;
}

Приведение некрасивое, но предупреждает, что при манипулировании битами лучше работать с беззнаковыми типами. Альтернативы до C ++ 20:boost::multiprecision::lsb .

Редактировать:

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

  • ваша версия: 5.7 с
  • Вторая версия KamilCuk: 4,7 с
  • моя версия: 4,7 с
  • Первая версия Эрика Постпищила: 4,3 с.
  • Версия Юрия Фельдмана (с явным использованием __builtin_popcount): 4,1 с

Так что, по крайней мере, на моей архитектуре, самым быстрым кажется тот, у которого есть popcount.

Изменить 2:

Я обновил свой тест, добавив новую версию Эрика Постпишила. Как просили в комментариях, код моего теста можно найти здесь . Я добавил цикл без операции, чтобы оценить время, необходимое для ГПСЧ. Я также добавил две версии от KevinZ. Код был скомпилирован на clang, -O3 -msse4 -mbmiчтобы получить popcntиblsi инструкции (спасибо Питеру Кордесу).

Результаты: По крайней мере, на моей архитектуре версия Эрика Постпищила в точности такая же быстрая, как версия Юрия Фельдмана, и как минимум в два раза быстрее, чем любая другая версия, предложенная до сих пор.

Джованни Черретани
источник
Я удалил операцию: return (x & x + (x & -x)) == 0;.
Эрик Постпищил
3
Это тест старой версии @Eric, верно? В текущей версии Eric's компилируется до 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 тратит впустую movи не может использовать преимущества lea): godbolt.org/z/5jeQLQ . С BMI1 версия Эрика по-прежнему лучше на x86-64, по крайней мере, на Intel, где blsiодин uop, но на AMD - 2 uop.
Питер Кордес
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 черезstd::popcount . C по-прежнему не имеет переносимого способа, который надежно компилируется в popcnt или аналогичную инструкцию для целей, где она доступна.

Юрий Фельдман
источник
2
Также самый быстрый на данный момент.
Джованни Черретани
2
Я думаю, вам нужно использовать беззнаковый тип, чтобы убедиться, что вы сдвигаете нули, а не копии знакового бита. Посмотрим 11011111. Арифметический сдвиг вправо, он становится 11101111, а XOR - 00110000. С логическим сдвигом вправо (сдвиг 0вверху) вы получаете 10110000и правильно обнаруживаете несколько битовых групп. Редактирование, чтобы исправить это.
Питер Кордес
3
Это действительно умно. Насколько мне не нравится стиль (IMO просто использую __builtin_popcount(), в настоящее время каждый компилятор имеет такой примитив), это, безусловно, самый быстрый (на современном процессоре). Фактически, я собираюсь утверждать, что эта презентация имеет серьезное значение, потому что на процессоре, который не имеет POPCNT в качестве единственной инструкции, моя реализация могла бы превзойти это. Поэтому, если вы собираетесь использовать эту реализацию, вам следует просто использовать встроенный. std::bitsetимеет ужасный интерфейс.
KevinZ 06
9

У процессоров есть специальные инструкции для этого, очень быстро. На ПК это BSR / BSF (введено в 80386 в 1985 году), на ARM - это CLZ / CTZ.

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

К сожалению, 35 лет не хватило, чтобы обновить язык C ++ до соответствия оборудованию. Чтобы использовать эти инструкции из C ++, вам понадобятся встроенные функции, они не переносимы и возвращают результаты в несколько других форматах. Используйте препроцессор и #ifdefт. Д., Чтобы обнаружить компилятор, а затем используйте соответствующие встроенные функции. В MSVC они _BitScanForward, _BitScanForward64, _BitScanReverse, _BitScanReverse64. В GCC и clang они есть __builtin_clzи __builtin_ctz.

Soonts
источник
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 и вращением. Жалко, что так много времени потребовалось, чтобы перенести битовое сканирование, но сейчас лучше, чем никогда. (Portable popcnt доступен через std::bitset::count().) В C ++ 20 все еще отсутствуют некоторые вещи, которые предоставляет Rust ( doc.rust-lang.org/std/primitive.i32.html ), например, обратный бит и порядок байтов, которые некоторые процессоры обеспечивают эффективно. но не все. Переносимая встроенная функция для операций, которые есть в любых процессорах, имеет смысл, хотя пользователям необходимо знать, что происходит быстро.
Питер Кордес
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, тогда у вас нет единиц, на ваше усмотрение, если вы считаете, что ответ будет да или нет
  • ничего другого: ответ отрицательный

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

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«компактно» ли оно.

CiaPan
источник
2

Мы можем использовать встроенные инструкции gcc, чтобы проверить:

Количество установленных битов

int __builtin_popcount (unsigned int x)
Возвращает количество 1-битов в x.

равно (a - b):

a : Индекс самого высокого установленного бита (32 - CTZ) (32, потому что 32 бита в целом числе без знака).

int __builtin_clz (unsigned int x)
Возвращает количество ведущих 0-битов в x, начиная с позиции самого старшего бита. Если x равен 0, результат не определен.

b : Индекс самого младшего установленного бита (CLZ):

int __builtin_clz (unsigned int x)
Возвращает количество ведущих 0-битов в x, начиная с позиции самого старшего бита. Если x равен 0, результат не определен.

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

который также можно записать как:

Я не думаю, что это более элегантно или эффективно, чем текущий ответ, получивший наибольшее количество голосов:

со следующей сборкой:

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

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

Уолтер
источник