У меня есть код, который более или менее похож на этот:
#include <bitset>
enum Flags { A = 1, B = 2, C = 3, D = 5,
E = 8, F = 13, G = 21, H,
I, J, K, L, M, N, O };
void apply_known_mask(std::bitset<64> &bits) {
const Flags important_bits[] = { B, D, E, H, K, M, L, O };
std::remove_reference<decltype(bits)>::type mask{};
for (const auto& bit : important_bits) {
mask.set(bit);
}
bits &= mask;
}
Clang> = 3.6 делает умную вещь и компилирует это в одну and
инструкцию (которая затем вставляется везде):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
Но каждая версия GCC, которую я пробовал, компилирует это до огромного беспорядка, который включает обработку ошибок, которая должна статически выполняться DCE. В другом коде он даже поместит important_bits
эквивалент в виде данных в соответствии с кодом!
.LC0:
.string "bitset::set"
.LC1:
.string "%s: __position (which is %zu) >= _Nb (which is %zu)"
apply_known_mask(std::bitset<64ul>&):
sub rsp, 40
xor esi, esi
mov ecx, 2
movabs rax, 21474836482
mov QWORD PTR [rsp], rax
mov r8d, 1
movabs rax, 94489280520
mov QWORD PTR [rsp+8], rax
movabs rax, 115964117017
mov QWORD PTR [rsp+16], rax
movabs rax, 124554051610
mov QWORD PTR [rsp+24], rax
mov rax, rsp
jmp .L2
.L3:
mov edx, DWORD PTR [rax]
mov rcx, rdx
cmp edx, 63
ja .L7
.L2:
mov rdx, r8
add rax, 4
sal rdx, cl
lea rcx, [rsp+32]
or rsi, rdx
cmp rax, rcx
jne .L3
and QWORD PTR [rdi], rsi
add rsp, 40
ret
.L7:
mov ecx, 64
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
xor eax, eax
call std::__throw_out_of_range_fmt(char const*, ...)
Как мне написать этот код, чтобы оба компилятора могли делать правильные вещи? В противном случае, как мне написать это, чтобы оно оставалось ясным, быстрым и удобным в обслуживании?
c++
c++11
bit-manipulation
Алекс Рейнкинг
источник
источник
B | D | E | ... | O
?(1ULL << B) | ... | (1ULL << O)
(1ULL << Constant)
| на строку и выровняйте имена констант на разных строках, чтобы было легче для глаз.int
результатом битовой операции МОЖЕТ БЫТЬint
ИЛИ можетlong long
зависеть от значения и формальноenum
не является эквивалентомint
константы. clang требует «как будто», gcc остается педантичнымОтветы:
Лучшая версия c ++ 17:
затем
обратно в C ++ 14, мы можем проделать этот странный трюк:
или, если мы застряли с C ++ 11, мы можем решить ее рекурсивно:
Godbolt со всеми 3 - вы можете переключить CPP_VERSION define, и получить идентичную сборку.
На практике я бы использовал самое современное, что мог. 14 лучше 11, потому что у нас нет рекурсии и, следовательно, длина символа O (n ^ 2) (что может резко увеличить время компиляции и использование памяти компилятора); 17 лучше 14, потому что компилятору не нужно удалять этот массив мертвым кодом, и этот трюк с массивом просто уродлив.
Из этих 14 самых запутанных. Здесь мы создаем анонимный массив всех нулей, тем временем в качестве побочного эффекта создаем наш результат, а затем отбрасываем массив. В отброшенном массиве есть количество нулей, равное размеру нашего пакета, плюс 1 (который мы добавляем, чтобы мы могли обрабатывать пустые пакеты).
Подробное объяснение того, что C ++ 14версия делает. Это уловка / хакерство, и тот факт, что вы должны сделать это для эффективного расширения пакетов параметров в C ++ 14, является одной из причин, почему выражения сворачивания были добавлены вc ++ 17.
Лучше всего это понять изнутри:
это просто обновляет
r
с1<<indexes
для фиксированного индекса.indexes
- это пакет параметров, поэтому нам придется его расширить.Остальная часть работы - предоставить пакет параметров для расширения
indexes
внутри.Один шаг вперед:
здесь мы приводим наше выражение к
void
, показывая, что нас не волнует его возвращаемое значение (нам просто нужен побочный эффект установкиr
- в C ++ такие выражения, какa |= b
также возвращают значение, которое они установилиa
).Затем мы используем оператор запятой
,
и,0
чтобы отброситьvoid
"значение" и вернуть значение0
. Итак, это выражение, значение которого есть,0
и в качестве побочного эффекта его вычисления0
устанавливается немногоr
.На этом этапе мы расширяем пакет параметров
indexes
. Получаем:в
{}
. Такое использование,
является не оператор запятой, а разделитель элемента массива. Этоsizeof...(indexes)+1
0
s, который также устанавливает биты вr
качестве побочного эффекта. Затем мы назначаем{}
массиву инструкции по построению массиваdiscard
.Далее мы приводим
discard
кvoid
- большинство компиляторов предупредит вас, если вы создадите переменную и никогда ее не прочитаете. Все компиляторы не будут жаловаться, если вы его приведетеvoid
, это своего рода способ сказать «Да, я знаю, я не использую это», поэтому предупреждение подавляется.источник
((1ull<<indexes)|...|0ull)
это «складное выражение» . В частности, это «двоичная правая складка», и ее следует анализировать как(pack
op
...
op
init)
Оптимизация, которую вы ищете, похоже, связана с отслаиванием петель, которое включается
-O3
или вручную-fpeel-loops
. Я не уверен, почему это подпадает под действие отслаивания цикла, а не развертывания цикла, но, возможно, он не хочет развернуть цикл с нелокальным потоком управления внутри него (как, возможно, есть из проверки диапазона).Однако по умолчанию GCC не может очистить все итерации, что, по-видимому, необходимо. Экспериментально, передача
-O2 -fpeel-loops --param max-peeled-insns=200
(значение по умолчанию 100) выполняет работу с вашим исходным кодом: https://godbolt.org/z/NNWrgaисточник
-O3 -fpeel-loops --param max-peeled-insns=200
не получается ... Это-ftree-slp-vectorize
видимо из-за .если использование только C ++ 11 является обязательным
(&a)[N]
, это способ захвата массивов. Это позволяет вам написать одну рекурсивную функцию без использования вспомогательных функций:присваивая его
constexpr auto
:Тест
Вывод
действительно нужно ценить способность C ++ вычислять все, что можно вычислить по Тьюрингу, во время компиляции. Это наверняка до сих пор поражает меня ( <> ).
Для более поздних версий C ++ 14 и C ++ 17 ответ yakk уже прекрасно охватывает это.
источник
apply_known_mask
самом деле оптимизирует?constexpr
. И хотя этого теоретически недостаточно, мы знаем, что GCC вполне способен выполнять оценку,constexpr
как задумано.Я бы посоветовал вам написать правильный
EnumSet
шрифт.Написание базового кода
EnumSet<E>
на C ++ 14 (и далее) на основеstd::uint64_t
тривиально:Это позволяет писать простой код:
В C ++ 11 это требует некоторых сверток, но тем не менее остается возможным:
И вызывается:
Даже GCC банально генерирует
and
инструкцию на-O1
Godbolt :источник
constexpr
кода незаконна. Я имею ввиду, у некоторых есть 2 утверждения! (C ++ 11 constexpr отстойный)EnumSet<E>
не использует значениеE
as напрямую, а вместо этого использует1 << e
. Это совершенно другой домен, что на самом деле делает класс таким ценным => нет шансов случайно проиндексироватьe
вместо1 << e
.Начиная с C ++ 11, вы также можете использовать классическую технику TMP:
Ссылка на Compiler Explorer: https://godbolt.org/z/Gk6KX1
Преимущество этого подхода перед функцией шаблона constexpr заключается в том, что он потенциально немного быстрее компилируется из-за правила Chiel .
источник
Здесь есть несколько далеких от "умных" идей. Вы, вероятно, не улучшите ремонтопригодность, следуя им.
является
намного проще написать чем
?
Тогда никакой остальной код не нужен.
источник