Как мне написать поддерживаемую, быструю битовую маску времени компиляции в C++?
У меня есть код, который более или менее похож на это:
#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'd. В другом коде, это будет даже поместить 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*, ...)
Как мне написать этот код, чтобы оба компилятора могли делать правильные вещи? В противном случае, как мне написать это, чтобы оно оставалось ясным, быстрым и понятным?
Ответы
Ответ 1
Лучшая версия - c++17:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return ((1ull<<indexes)|...|0ull);
}
затем
void apply_known_mask(std::bitset<64> &bits) {
constexpr auto m = mask<B,D,E,H,K,M,L,O>();
bits &= m;
}
Вернувшись в c++14, мы можем сделать этот странный трюк:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
auto r = 0ull;
using discard_t = int[]; // data never used
// value never used:
discard_t discard = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
(void)discard; // block unused var warnings
return r;
}
или, если мы застряли с c++11, мы можем решить это рекурсивно:
constexpr unsigned long long mask(){
return 0;
}
template<class...Tail>
constexpr unsigned long long mask(unsigned char b0, Tail...tail){
return (1ull<<b0) | mask(tail...);
}
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return mask(indexes...);
}
Годболт со всеми 3 - вы можете переключить CPP_VERSION определить и получить идентичную сборку.
На практике я бы использовал самое современное, что мог. 14 бьет 11, потому что у нас нет рекурсии и, следовательно, O (n ^ 2) длины символа (что может взорвать время компиляции и использование памяти компилятора); 17 бьет 14, потому что компилятору не нужно ничего кодировать, исключать этот массив, и этот трюк с массивом просто ужасен.
Из этих 14 самый запутанный. Здесь мы создаем анонимный массив всех нулей, а в качестве побочного эффекта создаем наш результат, а затем отбрасываем массив. У отброшенного массива есть число 0, равное размеру нашего пакета, плюс 1 (который мы добавляем, чтобы мы могли обрабатывать пустые пакеты).
Подробное объяснение того, что делает версия c++14. Это хитрость/хитрость, и тот факт, что вы должны сделать это для расширения пакетов параметров с эффективностью в C+ +1 4, является одной из причин, почему сложенные выражения были добавлены в c++17.
Это лучше всего понять изнутри:
r |= (1ull << indexes) // side effect, used
это просто обновляет r
с 1<<indexes
для фиксированного индекса. indexes
- это пакет параметров, поэтому нам придется его расширить.
Остальная часть работы заключается в предоставлении пакета параметров для расширения indexes
внутри.
Один шаг:
(void(
r |= (1ull << indexes) // side effect, used
),0)
здесь мы приводим наше выражение к void
, указывая, что мы не заботимся о его возвращаемом значении (мы просто хотим побочный эффект установки r
- в C+ +, выражения типа a |= b
также возвращают значение, для которого они устанавливают a
).
Затем мы используем оператор запятой ,
и 0
, чтобы отбросить void
"значение", и возвращает значение 0
. Так что это выражение, значение которого равно 0
и в качестве побочного эффекта вычисления 0
оно устанавливает бит в r
.
int discard[] = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
На этом этапе мы расширяем indexes
пакета параметров. Итак, мы получаем:
{
0,
(expression that sets a bit and returns 0),
(expression that sets a bit and returns 0),
[...]
(expression that sets a bit and returns 0),
}
в {}
. Это использование ,
не оператор запятой, а разделитель элемента массива. Это sizeof...(indexes)+1
0
с, который также устанавливает биты в r
как побочный эффект. Затем мы назначаем инструкции построения массива {}
для discard
массива.
Затем мы приводим discard
к void
- большинство компиляторов предупредит вас, если вы создадите переменную и никогда ее не прочитаете. Все компиляторы не будут жаловаться, если вы приведете его к void
, это своего рода способ сказать: "Да, я знаю, я не использую это", поэтому это подавляет предупреждение.
Ответ 2
Оптимизация, которую вы ищете, кажется, петлевая очистка, которая включена в -O3
, или вручную с -fpeel-loops
. Я не уверен, почему это относится к сфере очистки цикла, а не развертывания цикла, но, возможно, он не желает развернуть цикл с нелокальным потоком управления внутри него (как это, возможно, из проверки диапазона).
Однако по умолчанию GCC не может очистить все итерации, что, по-видимому, необходимо. Экспериментально, передача -O2 -fpeel-loops --param max-peeled-insns=200
(значение по умолчанию - 100) позволяет выполнить работу с вашим исходным кодом: https://godbolt.org/z/NNWrga
Ответ 3
если использование только C++ 11 является обязательным (&a)[N]
- это способ захвата массивов. Это позволяет вам написать одну рекурсивную функцию без использования вспомогательных функций:
template <std::size_t N>
constexpr std::uint64_t generate_mask(Flags const (&a)[N], std::size_t i = 0u){
return i < N ? (1ull << a[i] | generate_mask(a, i + 1u)) : 0ull;
}
присвоение его constexpr auto
:
void apply_known_mask(std::bitset<64>& bits) {
constexpr const Flags important_bits[] = { B, D, E, H, K, M, L, O };
constexpr auto m = generate_mask(important_bits); //< here
bits &= m;
}
Тестовое задание
int main() {
std::bitset<64> b;
b.flip();
apply_known_mask(b);
std::cout << b.to_string() << '\n';
}
Выход
0000000000000000000000000000000000101110010000000000000100100100
// ^ ^^^ ^ ^ ^ ^
// O MLK H E D B
действительно нужно ценить C++ способность вычислять что-либо вычисляемое во время компиляции. Это наверняка все еще поражает меня (<>).
В более поздних версиях C++ 14 и C++ 17 ответ Якка уже прекрасно охватывает это.
Ответ 4
Я бы посоветовал вам написать правильный тип EnumSet
.
Написание базового EnumSet<E>
в С++ 14 (далее) на основе std::uint64_t
тривиально:
template <typename E>
class EnumSet {
public:
constexpr EnumSet() = default;
constexpr EnumSet(std::initializer_list<E> values) {
for (auto e : values) {
set(e);
}
}
constexpr bool has(E e) const { return mData & mask(e); }
constexpr EnumSet& set(E e) { mData |= mask(e); return *this; }
constexpr EnumSet& unset(E e) { mData &= ~mask(e); return *this; }
constexpr EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
constexpr EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
std::uint64_t mData = 0;
};
Это позволяет вам написать простой код:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };
flags &= IMPORTANT;
}
В С++ 11 это требует некоторых сверток, но тем не менее остается возможным:
template <typename E>
class EnumSet {
public:
template <E... Values>
static constexpr EnumSet make() {
return EnumSet(make_impl(Values...));
}
constexpr EnumSet() = default;
constexpr bool has(E e) const { return mData & mask(e); }
void set(E e) { mData |= mask(e); }
void unset(E e) { mData &= ~mask(e); }
EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
static constexpr std::uint64_t make_impl() { return 0; }
template <typename... Tail>
static constexpr std::uint64_t make_impl(E head, Tail... tail) {
return mask(head) | make_impl(tail...);
}
explicit constexpr EnumSet(std::uint64_t data): mData(data) {}
std::uint64_t mData = 0;
};
И вызывается с помощью:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT =
EnumSet<Flags>::make<B, D, E, H, K, M, L, O>();
flags &= IMPORTANT;
}
Даже НКУ тривиальным генерирует and
инструкцию по -O1
godbolt:
apply_known_mask(EnumSet<Flags>&):
and QWORD PTR [rdi], 775946532
ret
Ответ 5
Начиная с С++ 11 вы также можете использовать классическую технику TMP:
template<std::uint64_t Flag, std::uint64_t... Flags>
struct bitmask
{
static constexpr std::uint64_t mask =
bitmask<Flag>::value | bitmask<Flags...>::value;
};
template<std::uint64_t Flag>
struct bitmask<Flag>
{
static constexpr std::uint64_t value = (uint64_t)1 << Flag;
};
void apply_known_mask(std::bitset<64> &bits)
{
constexpr auto mask = bitmask<B, D, E, H, K, M, L, O>::value;
bits &= mask;
}
Ссылка на Compiler Explorer: https://godbolt.org/z/Gk6KX1
Преимущество этого подхода перед шаблонной функцией constexpr заключается в том, что она потенциально немного быстрее компилируется из-за правила Чила.
Ответ 6
Здесь есть некоторые далеко не "умные" идеи. Вы, вероятно, не помогаете в обслуживании, следуя им.
является
{B, D, E, H, K, M, L, O};
гораздо проще написать, чем
(B| D| E| H| K| M| L| O);
?
Тогда никакой остальной код не нужен.