Обновить

Комментарии 72

@Exosphere пожалуйста, дайте работягам markdown вместо wysiwyg

Хорошая компиляция, на литкоде, кстати, много задач на битовые операции и алгоритмы

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

Есть такой типичный сценарий когда нужен LSB: есть стандартное решение задачи о коммивояжёре динамическим программированием на подмножествах. Суть в том, чтобы посчитать функцию "Какая длина минимального пути, проходящего по множеству вершин S и заканчивающегося в T?". Чтобы её вычислить нужно перебрать предпоследнюю вершины из S и взят минимум по путям S\{T}. Перебирать можно все биты и там все просто, а можно методом, который я писал в статье x = x & (x - 1), это в среднем уменьшает в 2 раза количество итераций. Проблема остается в том, что мы выделяем число, у которого ровно один бит, но не знаем какой, а он нужен для вычислений, вот здесь и нужен LSB. Проблема в целом в том, что с использованием LSB в таком сценарии итоговый выигрышь по производительности не очень велик (максимум в 2 раза) -- это значительно для продакшена, но в рамках олимпиадной задачи обычно незначительно.

(про разворот битов) В общем виде такой алгоритм имел бы сложность O(n log n).

А зачем он тогда такой нужен, если наивный алгоритм O(n)?

За De Bruijn метод спасибо, раньше он мне не попадался, а жаль, метод шикарен.

А зачем он тогда такой нужен, если наивный алгоритм O(n)?

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

Есть довольно известная книга Алгоритмические трюки для программистов. Там есть все это и еще многое другое.

О, спасибо! Добротная книга, в такой статье грех не сослаться на неё, добавил в описание. Единственное, метода Де Брюина я там не увидел.

Вроде как все описанные методы описаны в TAOCP vol. 4a Кнута, но при всём моем к нему уважении там это написано максимально непонятно

Имеет смысл добавить, что в оригинале она "Hackerʼs Delight", и под этим названием даже лучше известна в мире (и ищется по ссылкам).

Обновил.

Минус я случайно поставил пока тыкал тачпад. Не знаю как убрать

Это хабр вредный, на комментариях нельзя менять свои оценки. Спасибо за извещение.

НЛО прилетело и опубликовало эту надпись здесь

Для изменения порядка байтов в GCC есть __builtin_bswap16(), __builtin_bswap32(), __builtin_bswap64() (и где-то даже __builtin_bswap128()).

Для изменения порядка битов в Clang есть __builtin_bitreverse32() и т.п.; добавление в GCC пока только обсуждается.

В стандартном заголовке <bit> C++20 есть большинство описанных в статье манипуляций с битами, но нет разворота битов. Предложение добавить разворот битов я даже отправлял кому-то из комитета, но ответили - типа это не основная операция и добавлять не будем. Хотя чем она хуже тех же popcount() или countl_zero() ? ИМХО это невозможно объективно измерить и тут нужно использовать принцип функциональной полноты - операция достаточно фундаментальная, интуитивно очевидная, есть в некоторых архитектурах и в то же время программная реализация достаточно громоздка - значит нужно добавлять.

Я думаю, тут дело в количестве алгоритмов, зависимых от «разворота битов». Я не помню популярных алгоритмов, использующих такую операцию.

НЛО прилетело и опубликовало эту надпись здесь

Про байты - понятно. А вот биты - не очень.

Для чего применяется зеркалирование битмапов?

НЛО прилетело и опубликовало эту надпись здесь

bit/little endian - там про порядок байт в слове. Первое, что приходит в голову - hton/ntoh на "стандартном" x86. А вот чтобы прямо на уровне битов разворачивать - вроде сильно экзотично; пока не сталкивался.

bit/little endian - там про порядок байт в слове.

Не совсем, мягко говоря. IEN 137, который ввёл эти термины, явно связывает и с порядком нумерации бит. И это он не сам придумал, а отражает практику.

Вот пример: структура данных - битовый массив (bitset). Делаем код доступа:

#define ACCESS_TYPE uint8_t
#define SHIFT 3
#define LOW_MASK 7u
_Bool get_bit(void *bitset_area, unsigned index) {
  unsigned offset = index >> SHIFT;
  unsigned shift = index & LOW_MASK;
  // специально разложил по отдельным переменным для максимальной понятности...
  ACCESS_TYPE memvalue = ((ACCESS_TYPE*) bitset_area)[offset];
  ACCESS_TYPE mask = 1 << shift;
  return 0 != (memvalue & mask);
}

Теперь если у нас LE, мы можем менять: uint8_t, 3 в сдвиге индекса, 7 в маске - на, соответственно: uint16_t, 4, 15; uint32_t, 5, 31; uint64_t, 6, 63 - будет тот же результат.

А вот если платформа - BE, так не получится, от смены размера используемого значения всё поплывёт, результаты будут отличаться. Надо править. Проще всего поставить в вычислении shift - написать ~index вместо index, тогда они снова будут одинаковыми (в пределах BE - но отличаться от LE).

В компиляторах это ещё отражается, например, в следующем: цитата из <netinet/ip.h> линукса:

struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
...

Потому что для LE принято, аналогично описанному с bitset, что битовые поля заполняют отведённое место начиная с младших битов, а для BE - со старших.

В тех случаях, когда нумерация битов явно присутствует в машинных командах, она может быть как 0=LSB (совместимая с LE), так и 0=MSB (совместимая с BE). Так вот, платформа SystemZ (aka IBM Z; потомок S/360), которая последовательно BE, использует (команды типа RISBG) именно 0=MSB. Аналогично у POWER, который разрабатывался как BE, хотя может с памятью работать как LE. В документации у неё тоже 0=MSB. В результате надо следить: одна и та же часть регистра может обозначаться 0..31, если речь про 32-битную часть, и 32..63, если смотрим как на полный 64-битовый.

Тут много таки злобных гитик спряталось...

Ну вот на практике - пишем в заголовок файла магическую константу - число 0x58485053. Т.е. берём адрес числа, и выводим в поток 4 байта. На LE получается символьная последовательность "SPHX"

Собираем бинарь под MIPS, запускаем - в файле оказывается в начале "XHPS". Т.е. порядок байт поменялся, но при этом порядок бит в байте остался тем же (иначе были бы уже совсем другие символы).

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

ну, LE/BE это тоже про endianess.

Я о том, что всё ограничивается байтсвапом. А биты в байте задом наперёд, в основном, не перекладывают. MIPS может и адаптируется, но в каких-то нетривиальных конфигах. А на тех же SOHO роутерах, где остался - там есть MIPS, есть MIPSel, и с точки зрения пользователя с этим ничего не сделаешь.

НЛО прилетело и опубликовало эту надпись здесь

Да, если у вас дискрет рассмотрения - один байт (один символ ASCII), проблему можно и не заметить.

Хотя и в этом случае могут быть тонкости. Например, CRC чаще считается в порядке от младших битов в каждом байте (ибо традиция от RS-232), но бывает и наоборот.

Это уже сильно глубокая абстракция.
Байт - он вроде как везде одинаков.
В смысле, что абсолютное (числовое) значение не зависит от порядка бит. Разные операции (&, |, сдвиги) - тоже.
Копирования вроде того же memcpy - копируют целиком байты.
При сериализации (если нужно его побитно записать куда-то) я программно могу двигать в любом удобном направлении. Кажется, единственный момент, где иной порядок бит может выстрелить - это если есть аппаратная сериализация. Где я говорю железу - вот, сериализуй мне этот байтик. А оно внезапно - где-то младшим битом вперёд, где-то старшим. Другие способы "обнаружить" иной порядок бит не приходят в голову.
(не, ну, конечно, если сдвиги тоже "сломаны" - в смысле, есть 0x1, я выполняю 0x1 >> 1 (и ожидаю получить ноль), но вдруг получаю 2, потому что единственный значащий бит оказался крайним левым, и я переместил его на одну позицию вправо... Но это же совсем контр-интуитивно, ведь правда?)

Байт - он вроде как везде одинаков.
В смысле, что абсолютное (числовое) значение не зависит от порядка бит. Разные операции (&, |, сдвиги) - тоже.

Но я описал, как понимание группы байт как одного значения или нескольких - начинают давать зависимость от того, с какой стороны мы считаем биты.

Это не очень очевидно.
Если в одном байте мы не можем определить порядок бит, то что здесь меняет последовательность?
Там те же байты. Её можно перебирать (итерировать) с начала, либо с конца. Но там по-прежнему те же байты.
Как у них там внутри расположены биты - вроде неважно, покуда если я записываю в файл байт с символом "S" - он остаётся им самым, независимо от endianess платформы

Если в одном байте мы не можем определить порядок бит, то что здесь меняет последовательность?

Почему не можем? Можем. Для каждого бита кроме самого старшего есть один и только один, который для него следующий по старшинству вверх. И наоборот.

И это определение не зависит от порядка байт на машине. А вот на что он влияет - это как правильно стыковать группы бит из разных байт в более крупную единицу ("слово" и тому подобные).

Как у них там внутри расположены биты - вроде неважно, покуда если я записываю в файл байт с символом "S" - он остаётся им самым, независимо от endianess платформы

Я описал, для чего это становится важно.

Это уже сильно глубокая абстракция.
Байт - он вроде как везде одинаков.
В смысле, что абсолютное (числовое) значение не зависит от порядка бит. Разные операции (&, |, сдвиги) - тоже.
Копирования вроде того же memcpy - копируют целиком байты.
При сериализации (если нужно его побитно записать куда-то) я программно могу двигать в любом удобном направлении. Кажется, единственный момент, где иной порядок бит может выстрелить - это если есть аппаратная сериализация. Где я говорю железу - вот, сериализуй мне этот байтик. А оно внезапно - где-то младшим битом вперёд, где-то старшим. Другие способы "обнаружить" иной порядок бит не приходят в голову.
(не, ну, конечно, если сдвиги тоже "сломаны" - в смысле, есть 0x1, я выполняю 0x1 >> 1 (и ожидаю получить ноль), но вдруг получаю 2, потому что единственный значащий бит оказался крайним левым, и я переместил его на одну позицию вправо... Но это же совсем контр-интуитивно, ведь правда?)

Быстрое преобразование фурье. Какое-нибудь хеширование. Также эта перестановка вылезает в разных структурах данных для группировки значений.

С хешированием проблема курицы и яйца: применяют имеющиеся быстрые операции.

Не очень понимаю, как в преобразовании Фурье это применяется. Простые примеры реализации вообще без битовых операций. Но, видимо, есть какие-то супер-быстрые.

А какие структуры данных вы имеете в виду? Я интересуюсь структурами данных, но упоминание перестановки битов встречал один раз, и то не помню, в каком контексте.

В быстром перобразовании фурье сначала все элементы перемешивают кладя i-ый на место revbits(i). Про хеширование с аргументом вашим в целом согласен. Про структуры дынных вроде смутно помню, что в каких-то деревьях номер элемента описывал положение элемента в дереве и там битовыми операциями включая разворот можно было быстро находить наименьшего общего предка элементов, но как-то сформулировать не могу.

смутно помню, что в каких-то деревьях номер элемента описывал положение элемента в дереве и там битовыми операциями включая разворот можно было быстро находить наименьшего общего предка элементов

Скорее всего речь идет о представлении дерева виде правильной скобочной последовательности -- одна из основ succint структур данных. Детально не подскажу, но там точно битовых трюков используется. Конкретно реверс битов я нашел немного в другой штуке -- wavelet tree

https://github.com/simongog/sdsl-lite/blob/master/include/sdsl/wt_pc.hpp#L844

Если честно то я и для popcount() и поиска первого единичного/нулевого бита не знаю применений. Но это не делает эти операции менее важными - я интуитивно понимаю что они достаточно фундаментальны с точки зрения computer science, чтобы их включить в библиотеку. А разворот битов однажды доводилось применять - в одном протоколе биты в некотором числовом поле нужно было развернуть, для какой-то древней аппаратуры.

и поиска первого единичного/нулевого бита не знаю применений.

Определение двоичного порядка - например, важная промежуточная операция для вычислений с плавающей точкой, этого уже достаточно, чтобы делать аппаратный ускоритель ("приоритетный энкодер"). Вспомогательное действие для оценки порядка - например, для определения глубины стека для сортировок. Или определение, влезает ли число в поле, отведённое для него (хотя это можно делать через два сдвига и сравнение).

Но последние добавки этих команд (например, lzcnt, tzcnt, popcnt в x86) не из этого, а из каких-то более специфических применений. ChatGPT подсказал про lzcnt, например, "Оптимизация иерархии Bounding Volume Hierarchy (BVH), где требуется быстро находить старший значащий бит." А вот popcnt: "Алгоритмы SHA, MD5, CRC используют POPCNT для быстрого подсчёта различий между битовыми представлениями данных. В шифровании по методу Блюма-Блюма-Шуба (BBS) и других генераторах случайных чисел POPCNT применяется для оценки энтропии." Я не проверял, насколько это им критично и насколько ускоряет, но примерно похоже на правду.

А разворот битов однажды доводилось применять - в одном протоколе биты в некотором числовом поле нужно было развернуть, для какой-то древней аппаратуры.

Ну так это решается через несколько сдвигов - and - or, за логарифмическую цену. Как и вся эта группа - за логарифм от битового размера. Просто имея реализацию в железе получается таки быстрее, а на процессорах в миллиарды транзисторов добавка таких модулей это незаметно для общей массы.

В начале статьи есть ссылки на 4 опен сорс проекта, где они используются

Я использовал поиск последнего единичного бита для сжатия целых чисел, в один битовый стрим писались обрезанные до последнего значащего бита числа, в другой длинна  и там работал арифметический кодер сжимавший длинны, а все это вместе применялось в сжатии индекса в СУБД. 

Для байтов: __builtin_bswap. Который может разворачиваться и в код типа того, что был в статье, и в машинные команды (если ISA поддерживает), и вычисляться во время компиляции. Более того, LLVM и gcc распознают некоторые варианты рукописных реализаций этого дела и заменяют их на интринсик. Интринсик платформонезависимый, но не компиляторонезависимый, увы; но оно стандартизировано в C++: std::byteswap, в Rust (_type_::swap_bytes). А в C пока нет.

Та же ситуация с popcount, clz/ctz из статьи. Для разворота битов как-то хуже.

Кстати, в архитектуре x86/x64 большинство этих функций уже есть. Для манипуляций битами по индексу BT/BTS/BTC, для поиска единичных битов (BSF, BSR), для подсчета единичных битов (POPCNT). А вот инструкции разворота битов в байте (RBIT) почему-то нет (но зато она есть в ARM). А жаль - было бы хорошо иметь полный набор операций над битами в процессоре, и заодно в виде операций языка программирования. Скажем, унарные << и >> можно было бы приспособить для bit scans (поиск номера единичного бита с начала и с конца слова), бинарные <<< и >>> для битовых вращений (ROR и ROL) и т.д.

Они может и есть, но лучше лишний раз побенчить.
Я пробовал popcnt вставить в код - внезапно, самописный велосипед оказался быстрее в реальном алгоритме. Т.е. сам по себе он вроде как есть, но когда используется в выражении с другими операциями, конечный результат вычисляется медленнее.

Благодарю за статью.

Можете пояснить по бенчмаркам: что в колонках? Например первая строка - 1 итерация и 45.5 секунд - это что?

Бенчмарки сделаны с помощью google benchmark, в статье приведен его консольный вывод, вот он же на гитхабе

Бенчмарки где написано All 32-bit uint -- это прогон метода для всех 2^32 значений uint32_t. Одна итерация означает, что бенчмарк запускался один раз, 45.5 секунд -- время его выполнения. Есть несколько бенчмарков где прогоняется только 32 значения -- числа с одним единичным битом, они очень маленькие и google benchmark сам решает прогонять его несколько раз, число итераций он выбирает сам в этом случае.

Вот код бенчмарка

используется операция деления с остатком, которая обычно более вычислительно затратная, чем другие арифметические действия, впрочем очень похоже, что в современных компиляторах это не так

На самом деле операция и правда затратная, но в данном случае делитель константный, поэтому компилятор может выразить деление через несколько дешевых операций (умножения и сдвиги).

Погодите, а как такое произошло? Код прогоняется вот таким циклом

      for (uint32_t x = 0; x != (1 << 24); ++x) {                              \
        for (size_t i = 1; i < 32; ++i) {                                      \
          benchmark::DoNotOptimize(method(x, i));                              \
        }                                                                      \
      }

https://github.com/Malkovsky/bit_tricks/blob/main/src/benchmarks.cpp#L19C1-L23C8

метод benchmark::DoNotOptimize не запрещает внутренние оптимизации, но как будто в метод не константы передаются. Непонятно

Там же внутри остаток от деления на 37 - константу. DoNotOptimize просто не дает компилятору выкинуть вызов, потому что результат его работы, вообще говоря, игнорируется.

Возможно компилятор умеет деление на 2^k-1 оптимизировать так же, как деление на 2^k. Компилятор распознал и заменил наивный метод на какой-то еще более оптимальный. Надо смотреть ассемблерный выхлоп. Так-то операция % очень медленная в общем случае. Много раз заменял ее на вычитание и все очень сильно ускорялось.

Или это в процессоре оптимизация? Какая версия компилятора у вас там? Дизассемблер последнего gcc показывает наивную реализацию.

GCC 13.3

Локально на ноуте MSVC -- та же история, % лучше остальных

Интересно, там тоже нет оптимизаций в ассемблере. Значит, деление оптимизированно на уровне микрокода процессора. Попробуйте запустить тот же бенчмарк, но поменять в коде знаменатель и делить не на (1 << k)-1, а на, допустим, на k+1.

Та статья безнадёжно устарела:

Инструкция POPCNT из SSE4.2 не включена в список тестирования, потому что у меня нет процессора, который поддерживает SSE4.2.

Почему в текущей статье нет ни слова о POPCNT я не знаю.

Давайте отвечу тут на это и соседнее сообщение. на уровне "__builtin_popcount вызывает процессорную инструкцию" я как раз понимаю, но вот что там внутри уже нет. Подсчет единичных битов с помощью __builtin_popcount включен в сравнение

Чего я не знал до написания статьи -- того, что __builtin_popcount компилируется в POPCNT только при указании директивы #pragma GCC target("popcnt"), а без неё в call    __popcountdi2. За это спасибо внимательному зрителю

https://godbolt.org/z/yQv94Y

И да, с popcnt там все на порядок быстрее.

__builtin_popcount компилируется в POPCNT только при указании директивы #pragma GCC target("popcnt"),

Эээ? Я думал что достаточно передать -march в котором инструкция есть.

UPD: возможно надо передать что-то из -msse2/-msse3/-msse4. Но надеюсь что достаточно -march.

UPD2: https://godbolt.org/z/aYYb9a6M6

Ещё по-хорошему, если мы стремимся прям выжать из имеющегося железа всё что только можно, надо в бенчмарках фигачить -march=native. Да, возможно мы получим что-то сильно ограниченно переносимое, но цель же не в переносимости, цель в том шоб быстро было?

И вы тогда гляньте ещё моих ссылок, в современных процах куча инструкций для битовых операций, они покрывают (почти?) всё что вы перечисляете в статье.

Не, в этом то плане я не спорю, сам изначально написал

Точно могу сказать, что в случае их доступности надо использовать их

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

они покрывают (почти?)

скорее всего только select(x, i) не покрывается

В общем да, из-за того что вы не разрешили компилятору юзать SSE*, вы заставляете его выдавать ассемблер под что-то типа первых Athlon 64. Это очень, очень старьё.

самое время немного подправить замеры

Жду результатов :)

Благодарю!

Кстати, судя по таймингам, gcc догадался что All 32-bit uint/Count bits set iterate set bits можно скукожить до POPCNT.

Вот я тоже удивился с этого, но никак в дизасеблере не могу это воспроизвести

Хорошая статья, я очень много из неё повторяю, в общем то не секрет, что не я всё это придумал)

Добавил референс

Включаю в сравнение методы __builtin_popcount, __builtin_ctz , но понятия не имею что внутри происходит.

Ну как так-то? Погуглить жеж можно. Оно вызывает процессорные инструкции (при их наличии):

https://en.wikipedia.org/wiki/Find_first_set#Hardware_support
https://en.wikipedia.org/wiki/SSE4#POPCNT_and_LZCNT

alexey@alexey-mini:/opt/work/bit_tricks/build[main]$ ./bt_benchmarks
Unable to determine clock rate from sysctl: hw.cpufrequency: No such file or directory
This does not affect benchmark measurements, only the metadata output.
2025-02-28T19:47:35+07:00
Running ./bt_benchmarks
Run on (12 X 24.121 MHz CPU s)
CPU Caches:
  L1 Data 64 KiB
  L1 Instruction 128 KiB
  L2 Unified 4096 KiB (x12)
Load Average: 2.27, 2.87, 2.86
-------------------------------------------------------------------------------------------
Benchmark                                                 Time             CPU   Iterations
-------------------------------------------------------------------------------------------
All 32-bit uint/Count bits set naive                  44722 ms        44708 ms            1
All 32-bit uint/Count bits set iterate set bits        1247 ms         1247 ms            1
All 32-bit uint/Count bits set in parallel             3961 ms         3960 ms            1
All 32-bit uint/Count bits set with lookup table       2392 ms         2392 ms            1
All 32-bit uint/Count bits set with builtin            1261 ms         1261 ms            1
All 32-bit uint/Reverse naive                         43851 ms        43810 ms            1
All 32-bit uint/Reverse parallel                       1259 ms         1259 ms            1
All 32-bit uint/Reverse lookup                         3059 ms         3058 ms            1
All 24-bit uint/Select naive                           5619 ms         5617 ms            1
All 24-bit uint/Select parallel                        1407 ms         1407 ms            1
All 32-bit uint/LSB naive                              5532 ms         5532 ms            1
All 32-bit uint/LSB parallel                           6294 ms         6292 ms            1
All 32-bit uint/LSB float                              2061 ms         2061 ms            1
All 32-bit uint/LSB lookup                             2630 ms         2629 ms            1
All 32-bit uint/LSB mod lookup                         2820 ms         2820 ms            1
All 32-bit uint/LSB De Bruijn                          1258 ms         1258 ms            1
All 32-bit uint/LSB builtin ctz                        1254 ms         1254 ms            1
All 32-bit single bit uint/LSB naive                    178 ns          178 ns      3941619
All 32-bit single bituint/LSB parallel                 45.0 ns         45.0 ns     15750512
All 32-bit single bituint/LSB float                    13.0 ns         13.0 ns     53948241
All 32-bit single bit uint/LSB lookup                  29.3 ns         29.3 ns     24045150
All 32-bit single bituint/LSB mod lookup               21.5 ns         21.5 ns     31944799
All 32-bit single bituint/LSB De Bruijn               0.475 ns        0.475 ns   1000000000
All 32-bit single bituint/LSB builtin ctz             0.476 ns        0.476 ns   1000000000
All 24-bit uint/Division by 2^k-1 naive                 319 ms          319 ms            2
All 24-bit uint/Division by 2^k-1 cycle v1              946 ms          946 ms            1
All 24-bit uint/Division by 2^k-1 cycle v2             1285 ms         1284 ms            1
All 24-bit uint/Division by 2^k-1 lookup               2918 ms         2915 ms            1

Вот результаты с Apple M2 Max
Подозрительно выглядят 37 и 38. Либо они на arm64 в самом деле настолько быстрые, либо там что-то "порешал" компилятор.

Кстати, собралось не с первого раза. По дефолту бенч собирается в версии Debug, т.е. совсем не для бенча. А при явном указании RelWithDebInfo - сборка падает в недрах google-bench (пришлось туда лезть). Вы, наверное, там где-то добавили во флаги "все варнинги трактовать как ошибки". Не надо так.

Полагаю, что 37 и 38 -- это LSB De Bruijn и LSB builtin ctz. В GCC/Clang весь Де Брюин оптимизируется в tzctz. Почему именно на числах с одним единичным битом получается на порядок быстрее, чем в убунте -- хз.

Кстати, собралось не с первого раза. По дефолту бенч собирается в версии Debug, т.е. совсем не для бенча.

Так тип сборке вообще не указан CMakeLists.txt, это уже на усмотрение пользователя как он хочет собирать, для CI это вот тут прописано

https://github.com/Malkovsky/bit_tricks/blob/main/.github/workflows/cmake-multi-platform.yml#L53

А при явном указании RelWithDebInfo - сборка падает в недрах google-bench (пришлось туда лезть). Вы, наверное, там где-то добавили во флаги "все варнинги трактовать как ошибки". Не надо так.

google benchmark добавляется вот так и никаких патчей для него дальше нет, так что адресуйте претензии к google benchmark, который не собирается с -Werror

да, я уже посмотрел и убедился, что ваших доп. флагов там нет.
Но - при конфиге по дефолту (aka `mkdir build; cd build; cmake ..; cmake --build .` он собирает почему-то debug-версию. А при явном указании - валится, скорее всего из-за -Werror где-то)

Это уже есть в ссылках, большую часть кода я адаптировал оттуда.

Оказалось, что я забыл привести код для параллельного разворота битов, исправил.

UPD. Уже было обсуждение про разворот битов, но думаю стоит добавить: clang 19.1.0 компилирует __builtin_bitreverse64 в .... инструкцию vgf2p8affineqb. Кто не в курсе -- это инструкция общего вида для преобразования байта путем умножения его на битовую матрицу 8х8 с арифметкой поля GF(2). Предназначалось это в большей степени для реализации GF(2^8), но нашло и такое интересное применение

взято из Алиса копается

Когда мне надоедает заниматься перекладыванием джейсонов, и хочется почитать про битовые трюки и их применение, я иду читать Wojciech Muła: http://0x80.pl/notesen.html

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации