Я ищу эффективный способ определения позиции младшего значащего бита, который установлен в целое число, например, для 0x0FF0 это будет 4.
Тривиальная реализация такова:
unsigned GetLowestBitPos(unsigned value)
{
assert(value != 0); // handled separately
unsigned pos = 0;
while (!(value & 1))
{
value >>= 1;
++pos;
}
return pos;
}
Есть идеи, как выжать из него несколько циклов?
(Примечание: этот вопрос предназначен для людей, которым нравятся подобные вещи, а не для людей, которые говорят мне, что xyzоптимизация - зло.)
[править] Спасибо всем за идеи! Я узнал и кое-что еще. Прохладно!
c++
c
optimization
bit-manipulation
peterchen
источник
источник
Ответы:
Bit Twiddling Hacks предлагает отличную коллекцию, э-э, бит-твиддлинг-хаков, с прилагаемым обсуждением производительности / оптимизации. Мое любимое решение вашей проблемы (с этого сайта) - «умножь и ищи»:
Полезные ссылки:
источник
__builtin_ffsl
илиffsl
?Почему бы не использовать встроенную ffs ? (Я взял справочную страницу из Linux, но она более доступна, чем это.)
источник
Для этого существует инструкция по сборке x86 (
bsf
). :)Более оптимизировано ?!
Примечание:
Оптимизация на этом уровне по своей сути зависит от архитектуры. Современные процессоры слишком сложны (с точки зрения прогнозирования ветвлений, промахов в кэше, конвейерной обработки), поэтому так сложно предсказать, какой код на какой архитектуре выполняется быстрее. Уменьшение числа операций с 32 до 9 или тому подобное может даже снизить производительность на некоторых архитектурах. Оптимизированный код в одной архитектуре может привести к ухудшению кода в другой. Я думаю, вы либо оптимизируете это для конкретного процессора, либо оставите все как есть и позволите компилятору выбирать, что он считает лучшим.
источник
В большинстве современных архитектур есть инструкции для поиска позиции самого младшего установленного бита или самого высокого установленного бита, или подсчета количества ведущих нулей и т. Д.
Если у вас есть одна инструкция этого класса, вы можете дешево подражать другим.
Найдите минутку, чтобы проработать это на бумаге и понять, что
x & (x-1)
очистит самый младший установленный бит в x и( x & ~(x-1) )
вернет только самый младший установленный бит, независимо от архитектуры, длины слова и т. Д. Зная это, тривиально использовать аппаратное управление счетом -zeroes / upper-set-bit, чтобы найти самый младший установленный бит, если нет явной инструкции для этого.Если отсутствует соответствующая аппаратная поддержка вообще, реализация умножения и поиска нулевых значений с начальным числом, приведенная здесь или одна из тех, что на странице Bit Twiddling Hacks, может быть тривиально преобразована для получения самого низкого установленного бита с использованием указанных выше идентификаторов и имеет то преимущество, что он не имеет филиалов.
источник
Множество решений, а не ориентир. Вам должно быть стыдно ;-)
Моя машина - Intel i530 (2,9 ГГц), работающая под управлением 64-разрядной версии Windows 7. Я скомпилировал 32-битную версию MinGW.
Мой код:
источник
BSF
Имеет ложную зависимость от его вывода (поскольку фактическое поведение когда input = 0 должен оставить вывод без изменений). gcc, к сожалению, превращает это в зависимость с переносом цикла, не очищая регистр между итерациями цикла. Таким образом, цикл должен выполняться один раз на 5 циклов, узкое место на BSF (3) + CMOV (2) задержка.ffs()
пропускная способность должна составлять один за такт (3 мопа, 1 для BSF и 2 для CMOV, и они могут работать на разных портах). С такими же накладными расходами цикла это 7 мопов ALU, которые могут работать (на вашем процессоре) по 3 за такт. Над головой доминируют! Источник: agner.org/optimizebsf ecx, [ebx+edx*4]
не рассматриваетсяecx
как ввод, которого он должен ждать. (Последний раз ECX был написан CMOV предыдущего итератона). Но ЦП действительно ведет себя таким образом, чтобы реализовать поведение «оставить адрес без изменений, если источник равен нулю» (так что это не совсем ложное отклонение, как для TZCNT; требуется зависимость данных, потому что нет ветвления + спекулятивного выполнения в предположении что вход ненулевой). Мы могли бы преодолеть это, добавивxor ecx,ecx
передbsf
, чтобы сломать зависимость от ECX.Самым быстрым (не встроенным / не ассемблерным) решением этой проблемы является поиск самого младшего байта и последующее использование этого байта в таблице поиска с 256 записями. Это дает вам четыре условных инструкции в худшем случае и в лучшем случае 1. Это не только наименьшее количество инструкций, но и наименьшее количество ветвей, что очень важно на современном оборудовании.
Ваша таблица (256 8-битных записей) должна содержать индекс младшего разряда для каждого числа в диапазоне 0–255. Вы проверяете каждый байт своего значения и находите младший ненулевой байт, а затем используете это значение для поиска реального индекса.
Для этого требуется 256 байт памяти, но если скорость этой функции так важна, то 256 байт того стоят,
Например
источник
OMG это только что пошло по спирали.
Чего не хватает большинству этих примеров, так это небольшого понимания того, как работает все оборудование.
Каждый раз, когда у вас есть ветка, ЦП должен угадать, какая ветвь будет занята. Канал инструкций загружается инструкциями, ведущими по предполагаемому пути. Если ЦП ошибся, канал команд очищается, а другая ветвь должна быть загружена.
Рассмотрим простой цикл while вверху. Предполагается, что нужно оставаться в рамках цикла. Он будет ошибаться хотя бы один раз, когда выйдет из цикла. Это ПРОМЫВИТ трубу инструкций. Такое поведение немного лучше, чем предполагать, что он выйдет из цикла, и в этом случае он будет очищать конвейер команд на каждой итерации.
Количество потерянных циклов ЦП сильно варьируется от одного типа процессора к другому. Но вы можете ожидать от 20 до 150 потерянных циклов процессора.
Следующая худшая группа - это группа, в которой, по вашему мнению, вы сэкономите несколько итераций, разделив значение на более мелкие части и добавив еще несколько ветвей. Каждая из этих ветвей добавляет дополнительную возможность очистить конвейер команд и стоит еще от 20 до 150 тактов.
Давайте посмотрим, что происходит, когда вы ищите значение в таблице. Скорее всего, значение в настоящее время не находится в кеше, по крайней мере, не при первом вызове вашей функции. Это означает, что ЦП останавливается, пока значение загружается из кеша. Опять же, это варьируется от одной машины к другой. Новые чипы Intel фактически используют это как возможность поменять местами потоки, пока текущий поток ожидает завершения загрузки кеша. Это может быть намного дороже, чем промывка конвейера инструкций, однако, если вы выполняете эту операцию несколько раз, скорее всего, это произойдет только один раз.
Очевидно, что самое быстрое решение с постоянным временем - это решение, основанное на детерминированной математике. Чистое и элегантное решение.
Приношу свои извинения, если это уже было покрыто.
Каждый компилятор, который я использую, кроме XCODE AFAIK, имеет встроенные функции компилятора как для прямого, так и для обратного сканирования. Они будут компилироваться в единую инструкцию по сборке на большинстве аппаратных средств без каких-либо промахов в кэше, без предсказания ошибок ветвления и без каких-либо препятствий, создаваемых другими программистами.
Для компиляторов Microsoft используйте _BitScanForward & _BitScanReverse.
Для GCC используйте __builtin_ffs, __builtin_clz, __builtin_ctz.
Кроме того, воздержитесь от публикации ответов и потенциально вводящих в заблуждение новичков, если вы недостаточно осведомлены о обсуждаемой теме.
Извините, я полностью забыл предоставить решение .. Это код, который я использую на IPAD, в котором нет инструкции уровня сборки для задачи:
Здесь нужно понимать, что дорого обходится не сравнение, а ветвь, которая возникает после сравнения. В этом случае для сравнения принудительно устанавливается значение 0 или 1 с .. == 0, и результат используется для объединения математических расчетов, которые произошли бы на любой стороне ветви.
Редактировать:
Приведенный выше код полностью не работает. Этот код работает и по-прежнему не имеет ветвей (если оптимизирован):
Это возвращает -1, если задано 0. Если вас не интересует 0 или вы хотите получить 31 вместо 0, удалите вычисление i0, сэкономив кусок времени.
источник
-O3
godbolt.org/z/gcsUHdВдохновленный этим похожим постом, в котором идет поиск установленного бита, я предлагаю следующее:
Плюсы:
Минусы:
Обновление: как указано в комментариях, объединение является более чистой реализацией (по крайней мере, для C) и будет выглядеть так:
Это предполагает 32-битные целые числа с прямым порядком хранения для всего (подумайте о процессорах x86).
источник
int
естьint32_t
, и этот знаковый сдвиг вправо является арифметическим сдвигом (в C ++ он определяется реализацией)Это может быть сделано в худшем случае менее 32 операций:
Принцип: проверка 2 или более бит так же эффективна, как проверка 1 бита.
Так, например, ничто не мешает вам проверить, какая из групп находится в первую очередь, а затем проверить каждый бит от самого маленького до самого большого в этой группе.
Итак ...
если вы проверяете 2 бита за раз, у вас в худшем случае (Nbits / 2) + 1 проверка всего.
если вы проверяете 3 бита за раз, у вас в худшем случае (Nbit / 3) + 2 проверки всего.
...
Оптимальным было бы проверять группами по 4. Что потребует в худшем случае 11 операций вместо ваших 32.
Если вы используете эту идею группировки, лучший вариант - от 1 проверки ваших алгоритмов до 2 проверок. Но этот лишний 1 чек в лучшем случае того стоит в худшем случае.
Примечание: я пишу его полностью вместо использования цикла, потому что так он более эффективен.
источник
Почему бы не использовать двоичный поиск ? Это всегда будет завершено после 5 операций (при условии, что размер int равен 4 байтам):
источник
Другой метод (деление по модулю и поиск) заслуживает особого упоминания здесь по той же ссылке, предоставленной @ anton-tykhyy. Этот метод очень похож по производительности на метод умножения и поиска ДеБрюйна с небольшим, но важным отличием.
деление по модулю и поиск
метод деления по модулю и поиска возвращает разные значения для v = 0x00000000 и v = FFFFFFFF, тогда как метод умножения и поиска DeBruijn возвращает ноль на обоих входах.
тест:-
источник
mod
медленный. Вместо этого, вы можете использовать оригинальный метод умножения-и-подстановки и вычесть!v
изr
обрабатывать крайние случаи.Согласно странице BitScan программирования шахмат и моим собственным измерениям, вычитание и xor быстрее, чем отрицание и маска.
(Обратите внимание, что если вы собираетесь считать конечные нули
0
, метод, который у меня есть, возвращается,63
тогда как отрицание и маска возвращаются0
.)Вот 64-битное вычитание и xor:
Для справки, вот 64-битная версия метода отрицания и маски:
источник
(v ^ (v-1))
работает при условииv != 0
. В случае, еслиv == 0
он возвращает 0xFF .... FF при этом(v & -v)
дает ноль (что, кстати, тоже неверно, но, по крайней мере, это приводит к разумному результату).v ^ (v-1)
, поэтому их невозможно отличить. В моем сценарии ноль никогда не вводится.Вы можете проверить, установлен ли какой-либо из младших битов. Если да, то посмотрите на младшие оставшиеся биты. например,:
32bit int - проверьте, установлены ли какие-либо из первых 16. Если да, проверьте, установлены ли какие-либо из первых 8. если так, ....
если нет, проверьте, установлен ли какой-либо из верхних 16.
По сути, это бинарный поиск.
источник
См. Мой ответ здесь, чтобы узнать, как это сделать с помощью одной инструкции x86, за исключением того, что для нахождения наименее значимого установленного бита вам понадобится
BSF
инструкция («перемотка вперед») вместоBSR
описанной там.источник
Еще одно решение, не самое быстрое, но кажется неплохим.
По крайней мере, у него нет веток. ;)
источник
1
s от наименее значащей 1 до LSB, используйте((x & -x) - 1) << 1
вместо этогоx ^ (x-1)
50% всех чисел вернутся в первой строке кода.
75% всех чисел вернутся в первых 2 строках кода.
87% всех чисел вернутся в первых 3 строках кода.
94% всех чисел вернутся в первых 4 строках кода.
97% всех чисел вернутся в первых 5 строках кода.
и т.п.
Я думаю, что люди, которые жалуются на то, насколько неэффективен наихудший сценарий для этого кода, не понимают, насколько редко может произойти это состояние.
источник
Нашел этот хитрый трюк, используя «волшебные маски» в «Искусство программирования, часть 4», который делает это за O (log (n)) времени для n-битного числа. [с дополнительным пространством log (n)]. Типичные решения для проверки установленного бита - либо O (n), либо требуется O (n) дополнительного места для справочной таблицы, так что это хороший компромисс.
Волшебные маски:
Ключевая идея: Нет нулей в конце в x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...
источник
Если вам доступен C ++ 11, компилятор иногда может сделать эту задачу за вас :)
Результат - индекс, отсчитываемый от 1.
источник
ffs()
во время компиляции, поэтому вам не нужно использовать это для работы с постоянным распространением. (Вы должны избегать инлайн-ассемблера, конечно.) Если вам действительно нужно что - то , что работает как C ++ 11constexpr
, вы можете использовать GNU C__builtin_ffs
.Это по поводу ответа @Anton Tykhyy
Вот моя реализация constexpr на C ++ 11, в которой устранены приведения типов и предупреждение на VC ++ 17 путем усечения 64-битного результата до 32-битного:
Чтобы обойти проблему 0x1 и 0x0, оба возвращают 0, вы можете сделать:
но если компилятор не может или не будет предварительно обрабатывать вызов, он добавит пару циклов к вычислению.
Наконец, если интересно, вот список статических утверждений, чтобы проверить, что код выполняет то, для чего предназначен:
источник
Вот одна простая альтернатива, хотя поиск журналов стоит немного дороже.
источник
Недавно я увидел, что премьер Сингапура разместил свою программу на фейсбуке, есть одна строчка, чтобы упомянуть об этом ...
Логика - это просто "значение и -значение", предположим, у вас есть 0x0FF0, затем 0FF0 & (F00F + 1), что равно 0x0010, что означает, что наименьшая 1 находится в 4-м бите .. :)
источник
Если у вас есть ресурсы, вы можете пожертвовать памятью ради повышения скорости:
Примечание. Эта таблица будет занимать не менее 4 ГБ (16 ГБ, если мы оставим тип возвращаемого значения как
unsigned
). Это пример обмена одного ограниченного ресурса (ОЗУ) на другой (скорость выполнения).Если ваша функция должна оставаться переносимой и работать как можно быстрее любой ценой, это будет лучший вариант. В большинстве реальных приложений таблица размером 4 ГБ нереальна.
источник
:)
@Dan: Вы правы насчет кеширования памяти. См. Комментарий Майкиджа выше.