Самый эффективный способ проверить, является ли положительное целое число 2^n (т.е. 1, 2, 4, 8 и т. д.) в C++20?

Удобный метод проверки того, является ли положительное целое число n степенью двойки (например, 1, 2, 4, 8 и т. д.), заключается в использовании следующего теста на наличие не более 1 бита:

bool test = n & (n - 1) == 0;

Эта операция может быть очень эффективной, поскольку она включает в себя только вычитание, побитовое И и условный переход по нулевому флагу (ZF). Если это выражение равно true, то число n действительно является степенью двойки.

Другой метод использует для проверки функцию std::popcount (подсчет населения), которая является частью стандартной библиотеки C++20:

bool test = std::popcount(n) == 1; // (Since C++20)

Эта функция подсчитывает количество установленных битов (1 с). Если аппаратное обеспечение поддерживает инструкцию popcount (POPCNT), эта функция может работать очень быстро.

В C++ вы обычно «платите за то, что используете». В этом тесте счет не нужен.

Какой метод лучше с точки зрения эффективности процессора?

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

463035818_is_not_an_ai 12.06.2024 16:04

Единственный реальный способ узнать это — протестировать оба и сравнить, если сборка не одинакова.

NathanOliver 12.06.2024 16:04

Какое оборудование? Какая эффективность, эффективность памяти, энергоэффективность,...?

Dominic Hofer 12.06.2024 16:07

Обратите внимание, что std::popcount(n) == 1/std::has_single_bit(n) эквивалентно ((n & (n-1)) == 0) && (n != 0)

Artyer 12.06.2024 16:10

Как всегда, это зависит от вашей конкретной конфигурации оборудования (не только от C++). Итак, вам нужно измерить

Pepijn Kramer 12.06.2024 17:22

Почему это должно быть эффективно? Какой диапазон эффективности вам нужен?

Thomas Matthews 12.06.2024 19:37

На x86-64-v3 (который включает BMI1 + BMI2) у вас есть blsr, который выполняет n & (n-1) (очистку младшего установленного бита) в одной инструкции и устанавливает ФЛАГИ полезным для ветки способом. Условия FLAGS могут даже отличить отсутствие набора битов (0) от одного набора битов: CF=1, если входной сигнал равен нулю, ZF=1, если выходной сигнал равен нулю (т. е. ZF содержит ваш bool test). Вы знаете, что ваше целое число положительное, поэтому ноль не является возможным входным значением, но многие варианты использования std::has_single_bit не могут этого предполагать.

Peter Cordes 17.06.2024 09:51
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
7
405
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

bool test = std::has_single_bit(n);

std::has_single_bit следует преобразовать в наиболее эффективную последовательность для текущей платформы.

template< class T >
constexpr bool has_single_bit( T x ) noexcept;

(начиная с С++20)

Проверяет, является ли x целой степенью двойки.

Эта перегрузка участвует в разрешении перегрузки, только если T является целочисленным типом без знака (то есть unsigned char, unsigned short, unsigned int, unsigned long, unsigned long long или расширенным целочисленным типом без знака).

Стоит отметить, что cppref показывает возможные реализации с точки зрения std::popcount(x) == 1; и варианта, похожего на OP. Хотя это, конечно, всего лишь «возможная реализация».

463035818_is_not_an_ai 12.06.2024 16:09

@ 463035818_is_not_an_ai На моей платформе библиотека C++20 определяет std::has_single_bit( T x ) через std::popcount(x) == 1. Алгоритм битовой обработки, который я представил ниже, является логикой сборки для popcount.

Edwin Buck 15.06.2024 15:54

@EdwinBuck: Хорошая реализация std::popcount будет использовать аппаратную инструкцию popcnt, если она доступна. Конечно, GCC и Clang распознают эту идиому SWAR bithack из вашего ответа (и Подсчитайте количество установленных бит в 32-битном целом) и оптимизируйте ее как операцию popcount, используя одну HW-инструкцию, если она доступна. Этот битхак в наши дни используется вспомогательной функцией libgcc.a GCC для целей без HW popcount (ранее 8-битный LUT), но писать свою собственную версию таким образом неразумно в C++20, хотя можно было бы использовать std::popcount

Peter Cordes 17.06.2024 09:57

@EdwinBuck: Такое определение has_single_bit как popcount(n) == 1 разумно, если вы не знаете, что оно положительное (гарантировано ненулевое). Если вы это знаете, то, по крайней мере, на x86 это намного дешевле n & (n-1) == 0, особенно с BMI1 blsr, который делает это за одну инструкцию. Также AArch64, где popcount требует копирования в векторные регистры и обратно, если у вас нет скаляра ARMv9.x cnt

Peter Cordes 17.06.2024 10:02

Насколько читаемым вы хотите свой код?

   x = x - ((x >> 1) & 0x55555555);
   x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
   x = (x + (x >> 4)) & 0x0F0F0F0F;
   x = x + (x >> 8);
   x = x + (x >> 16);
   if (x == 1) {
   }

Это алгоритм подсчета битов из «Hacker's Delight», старой сокровищницы полезных алгоритмов перебора битов.

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

выполняет попарное сложение битов. Обратите внимание, что

000 >>1 = 000, 000 & 101 = 0
001 >>1 = 000, 000 & 101 = 0
010 >>1 = 001, 001 & 101 = 1
011 >>1 = 001, 001 & 101 = 1
100 >>1 = 010, 010 & 101 = 0
101 >>1 = 010, 010 & 101 = 0
110 >>1 = 011, 011 & 101 = 1
111 >>1 = 011, 011 & 101 = 1

Таким образом, ((x >> 1) & 0x555555555) действует как «набор 2» каждых двух битов по порядку внутри слова.

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

Остальные строки затем выполняют попарное суммирование, складывая счетчики каждой пары «2 бит», суммируя счетчики каждой пары «4 бита», суммируя счетчик каждой пары «8 бит» и так далее. Если вам нужно сделать два 64-битных слова, вы можете добавить дополнительную строку.

Наконец, чтобы проверить, установлен ли у вас один бит, вы проверяете, равен ли счетчик 1.

Обратите внимание: хотя это и интересно рассматривать, лучше использовать std::has_single_bit, который использует тот же алгоритм (но параметризован для обработки слов разной длины и проверяет, что количество битов (алгоритм, о котором я говорю) равно 1. Приведенное выше решение предназначено для 32-битных слов, но его можно легко изменить для 64-битных слов.

https://github.com/gcc-mirror/gcc/blob/bd3a312728fbf8c35a09239b9180269f938f872e/libstdc%2B%2B-v3/include/std/bit#L325

Хакерский восторг https://learning.oreilly.com/library/view/hackers-delight- Second/9780133084993/

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

Edwin Buck 12.06.2024 16:36

Это также не ответ на этот вопрос, потому что он явно неэффективен (относительно уже предложенных ОП решений)

Pepijn Kramer 12.06.2024 17:24

@PepijnKramer Это точно такое же количество операций ЦП, как указано выше (поэтому логика имеет точно такую ​​​​же эффективность: x операций MMU для числа размером 2 ^ x и одна операция сравнения), но это решение без создания стек вызовов функций и уничтожение стека вызовов, что на самом деле более эффективно. Итак, в некотором смысле это более эффективно, но я бы все же попросил людей использовать std::has_single_bit, потому что он содержит имя, которое делает функцию очевидной относительно того, что она делает. --- На сегодняшний день этот алгоритм еще не побежден.

Edwin Buck 12.06.2024 17:48

Я думаю, что мое мнение здесь предвзятое (извиняюсь). В основном я использую процессоры x86, инструкция POPCNT имеет задержку 3 цикла / пропускную способность 1 цикл (так что это довольно эффективно). И да, это довольно крутой алгоритм :)

Pepijn Kramer 12.06.2024 20:00

самая читабельная версия bool test = n & (n - 1) == 0; как в ОП. Подсчитывать биты, а затем проверять, равно ли это 1, крайне неэффективно.

phuclv 13.06.2024 06:49

@phuclv n & n-1 == 0 даже близко не подходит к проверке того, установлен ли только один бит, например 0x08 и 0x07 = 0x0F, который не равен 1, тогда как 0x08, очевидно, установлен только один бит.

Edwin Buck 13.06.2024 07:03

@ЭдвинБак 0x08 & 0x07 = 0x0F? Хм? 0b01000 & 0b00111 не имеет перекрывающихся битов, поэтому это 0. 0x08 | 0x07 было бы 0x0F

Andrew Henle 13.06.2024 08:32

@AndrewHenle Вы правы, это будет 0. Наверное, я немного устал и слишком быстро отвечал, я использовал побитовое и вместо побитового или.

Edwin Buck 13.06.2024 09:52

Если у вас нет аппаратного popcount, вы определенно не хотите выполнять полный битхак popcount только для того, чтобы проверить, 1 это или нет. В C++20 нет смысла писать popcount для битхака вручную. За исключением, возможно, обхода плохой реализации std::popcount при компиляции для процессоров без встроенной инструкции popcnt. Но все же только в тех случаях, когда вам действительно нужен полный счетчик посещений, а не одна итерация битхака с очисткой самого младшего бита, n & (n-1) (например, x86 BMI1 blsr, который даже устанавливает ФЛАГИ таким образом, что позволяет вам определить, был ли установлен ровно 1 бит , вместо битов 0 или 1, если n&(n-1) == 0.)

Peter Cordes 17.06.2024 09:47
Ответ принят как подходящий

Вы знаете, что ваше число положительное (что исключает ноль), поэтому вы действительно можете просто использовать n & (n-1) == 0, не проверяя n != 0. Это ваш самый эффективный вариант, потенциально более эффективный, чем C++20 std::has_single_bit

std::has_single_bit необходимо исключить случай отсутствия набора битов. Для этого на современном x86 может быть немного эффективнее сделать popcount(n) == 1, если компилятор может предположить поддержку аппаратной popcnt инструкции, поэтому std::has_single_bit часто определяется именно так в стандартных библиотеках C++.

Но поскольку вы знаете, что ваше число не равно нулю, битхак наиболее эффективен. Особенно при компиляции для цели, где компилятор не может принять аппаратный popcount (например, x86 без -march=x86-64-v2 или новее) или AArch64 до ARMv9.x, где скалярный popcount требует копирования в векторные регистры и обратно. RISC-V имеет аппаратный счетчик всплывающих окон только в необычном расширении, а не в базовой версии.

На x86 это может быть так же дешево, как

  lea   eax, [rdi-1]
  test  eax, edi
   # boolean condition in ZF
  jz  or setz  or cmovz  or whatever

И аналогично AArch64 с sub и tst. И практически любой другой современный RISC может вычесть 1, помещая результат в отдельный регистр, а затем дешево И все вместе.


А если вы компилируете для -march=x86-64-v3 или более поздней версии (BMI2 + AVX2 + FMA = набор функций Haswell), компилятор может использовать BMI1 blsr eax, edi для очистки («сброса») младшего установленного бита и установки ФЛАГОВ соответственно, с ZF, установленным в соответствии с нулевым выходным сигналом. CF устанавливается, если входной сигнал равен нулю, поэтому некоторые условия ветвления могут это проверить n!=0. Но, к сожалению, такие условия, как jbe есть CF=1 or ZF=1, ja есть CF=0 and ZF=0. Не существует ни одного условия FLAGS, проверяющего ZF=1 и CF=0, которое позволило бы std::has_single_bit скомпилировать только blsr плюс одну ветку, cmov или setcc. ja берется, если на входе установлено несколько битов, не принимается для степени 2 или нуля.

В отличие от test, blsr не может объединяться макросом с более поздним jcc, поэтому он не сохраняет никаких ошибок по сравнению с lea/test, если вы разветвляете его. В Intel лучше, если компилятор будет использовать его независимо, например, для setnz или cmovnz. blsr — это одиночный процессор на Intel, AMD Zen 4 и более поздних версиях. 2 уопа на Zen 3 и более ранних версиях (https://uops.info/)


Непопсовый способ исключить ноль

Для случаев использования, когда вы не можете принять ненулевой ввод и не имеете дешевого аппаратного popcount, существует альтернативный битхак, который состоит из 3 операций вместо двух: (n - 1) < (n ^ (n - 1))

Правая часть — это то, что вычисляет x86 BMI1 blsmsk, но если у нас есть blsmsk, то у нас есть popcnt.
n-1 — это распространенное подвыражение, поэтому нам нужно вычислить его только один раз. Например RISC-V; AArch64 может быть похож на cmp/bltu.

 addi  x1, x0, -1             # n-1
 xor   x2, x1, x0             # n ^ (n-1)
 bltu  x1, x2, power_of_two   # branch on a comparison

Для нуля n-1 — это UINT_MAX, а n ^ anything — неактивный оператор, поэтому обе стороны равны.

Для степени двойки n-1 устанавливает все биты ниже того места, где был установленный бит, и XOR устанавливает этот бит снова. Итак, оно больше, чем n, а также больше, чем n-1.

Для числа, не являющегося степенью двойки, n ^ (n-1) по-прежнему является просто маской до самого младшего установленного бита включительно, при этом старшие биты отменены (те, которые n-1 не перевернулись). Значит, он меньше n-1.

https://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 также предлагает v && !(v & (v - 1));, но я не думаю, что это лучше, поскольку логическое && должно проверять обе стороны на ненулевое значение.

Кстати, есть еще один трюк «без popcnt», который все еще работает, если n == 0, а именно (n - 1) < (n ^ (n - 1)). Вещь справа может быть blsmsk, тогда в целом это может быть что-то вроде blsmsk, sub, cmp (это все равно лишняя операция по сравнению с тем, чтобы не заботиться о n == 0, конечно)

user555045 20.06.2024 15:25

@user555045: user555045: n-1 — это распространенное подвыражение, поэтому вы также можете сделать это с помощью lea / xor / cmp. Так что еще 3 инструкции даже без ИМТ1, или на ISA типа RISC-V (addi/xor/bgu). Круто, спасибо, что указали на это.

Peter Cordes 20.06.2024 22:29

Другие вопросы по теме

Похожие вопросы

Почему Clang запрещает использование квалификаторов в анонимных битовых полях?
Std::partition_copy: что происходит, когда диапазон вывода d_first_true перекрывается с диапазоном ввода?
Как мне обойти то, что кажется целочисленным переполнением, несмотря на то, что тип достаточно большой
Когда необходимо устранить неоднозначность между объявлениями и выражениями?
Захват битовых полей по ссылке в лямбда-выражении
Инициализировать unordered_set как статическую переменную-член вместо инициализации в конструкторе в C++
OpenCL – Как предотвратить появление ошибок сборки при переходе к стандартной ошибке?
Является ли вставка в вектор при одновременном доступе к вектору неопределенным поведением?
Почему указатели на элементы данных можно вызывать в C++?
Ошибка компиляции при использовании функций шаблона C++, которые принимают в качестве аргументов другие функции, которые принимают ссылки на указатели