Мне нужно проверить, образуют ли позиции (от 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
.
c++
c
bit-manipulation
Уолтер
источник
источник
0x0
компактно. Проще определить противоположное (не компактное): если есть два установленных бита, то есть хотя бы один неустановленный бит между ними.h>=l
по (подразумеваемой) функциональностиhighest_set_bit()
иlowest_set_bit()
Ответы:
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 бит выше.источник
x & -x
в однойblsi
инструкции, что составляет 1 мкоп для Intel и 2 мкоп для AMD Zen. godbolt.org/z/5zBx-A . Но без BMI1 версия @ KevinZ еще более эффективна._Bool
стандартное ключевое слово, согласно C 2018 6.4.1 1.unsigned
. Если вы хотите выполнить тест для подписанного дополнения до двухint
, самый простой способ - просто передать его подпрограмме в этом ответе, позволяяint
преобразовать вunsigned
. Это даст желаемый результат. Применение операций show к подписанномуint
напрямую может быть проблематичным из-за проблем с переполнением / переносом. (Если вы хотите проверить свое дополнение или знак и величинуint
, это другой вопрос, в настоящее время представляющий в основном только теоретический интерес.)На самом деле нет необходимости использовать какие-либо встроенные функции.
Сначала переверните все нули перед первым 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; }
источник
-mtbm
использованиемblsfill
/blcfill
инструкциями. Это будет самый короткий вариант из предложенных на данный момент. К сожалению, это расширение набора команд почти не поддерживает процессор .На самом деле вам не нужно считать ведущие нули. Как предлагает 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
:__builtin_popcount
): 4,1 сТак что, по крайней мере, на моей архитектуре, самым быстрым кажется тот, у которого есть popcount.
Изменить 2:
Я обновил свой тест, добавив новую версию Эрика Постпишила. Как просили в комментариях, код моего теста можно найти здесь . Я добавил цикл без операции, чтобы оценить время, необходимое для ГПСЧ. Я также добавил две версии от KevinZ. Код был скомпилирован на clang,
-O3 -msse4 -mbmi
чтобы получитьpopcnt
иblsi
инструкции (спасибо Питеру Кордесу).Результаты: По крайней мере, на моей архитектуре версия Эрика Постпищила в точности такая же быстрая, как версия Юрия Фельдмана, и как минимум в два раза быстрее, чем любая другая версия, предложенная до сих пор.
источник
return (x & x + (x & -x)) == 0;
.gcc -O3 -march=nehalem
минимального количества инструкций с (чтобы сделать popcnt доступным) или до меньшего, если BMI1blsi
доступен дляx & -x
: godbolt.org/z/zuyj_f . И все инструкции простые, однократные, за исключением версииpopcnt
Юрия, которая имеет задержку в 3 цикла. (Но я предполагаю, что вы увеличивали пропускную способность.) Я также предполагаю, что вы, должно быть, удалилиand val
с Юрия, иначе это будет медленнее.mov
и не может использовать преимуществаlea
): godbolt.org/z/5jeQLQ . С BMI1 версия Эрика по-прежнему лучше на x86-64, по крайней мере, на Intel, гдеblsi
один uop, но на AMD - 2 uop.Не уверен в скорости, но могу сделать однострочник, проверив, что у
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 или аналогичную инструкцию для целей, где она доступна.источник
11011111
. Арифметический сдвиг вправо, он становится11101111
, а XOR -00110000
. С логическим сдвигом вправо (сдвиг0
вверху) вы получаете10110000
и правильно обнаруживаете несколько битовых групп. Редактирование, чтобы исправить это.__builtin_popcount()
, в настоящее время каждый компилятор имеет такой примитив), это, безусловно, самый быстрый (на современном процессоре). Фактически, я собираюсь утверждать, что эта презентация имеет серьезное значение, потому что на процессоре, который не имеет POPCNT в качестве единственной инструкции, моя реализация могла бы превзойти это. Поэтому, если вы собираетесь использовать эту реализацию, вам следует просто использовать встроенный.std::bitset
имеет ужасный интерфейс.У процессоров есть специальные инструкции для этого, очень быстро. На ПК это 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
.источник
std::countr_zero
иstd::countl_zero
. Если вы используете Boost, у него есть переносимые оболочки, называемыеboost::multiprecision::lsb
иboost::multiprecision::msb
.#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 ), например, обратный бит и порядок байтов, которые некоторые процессоры обеспечивают эффективно. но не все. Переносимая встроенная функция для операций, которые есть в любых процессорах, имеет смысл, хотя пользователям необходимо знать, что происходит быстро.Сравнение с нулями вместо единиц сэкономит некоторые операции:
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
, что делаете то, что думаете?there exists a faster way?
и предположил, что все идет.Вы можете перефразировать требование:
Перебор всех битов может выглядеть так:
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).источник
Вы можете выполнить эту последовательность вычислений (при условии,
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
на одну позицию, чтобы получить немного ниже фактического LSBval
, и выполните ту же процедуру, что и для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
«компактно» ли оно.источник
Мы можем использовать встроенные инструкции gcc, чтобы проверить:
Количество установленных битов
равно (a - b):
a : Индекс самого высокого установленного бита (32 - CTZ) (32, потому что 32 бита в целом числе без знака).
b : Индекс самого младшего установленного бита (CLZ):
Например, если 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
но это, наверное, легче понять.
источник
Хорошо, вот версия, которая перебирает биты
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; }
Первые две петли нашли первую компактную область. Последний цикл проверяет, есть ли другой установленный бит за пределами этой области.
источник