Как мне написать поддерживаемую, быструю битовую маску времени компиляции на C ++?


114

У меня есть код, который более или менее похож на этот:

#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*, ...)

Как мне написать этот код, чтобы оба компилятора могли делать правильные вещи? В противном случае, как мне написать это, чтобы оно оставалось ясным, быстрым и удобным в обслуживании?


4
Вы не можете создать маску вместо цикла B | D | E | ... | O?
— HolyBlackCat

6
В перечислении есть позиции битов, а не уже развернутые биты, так что я мог бы это сделать,(1ULL << B) | ... | (1ULL << O)
— Алекс

3
Обратной стороной является то, что фактические имена длинные и неправильные, и не так легко увидеть, какие флаги находятся в маске со всем этим линейным шумом.
— Alex Reinking

4
@AlexReinking Вы могли бы сделать это одним (1ULL << Constant)| на строку и выровняйте имена констант на разных строках, чтобы было легче для глаз.
— einpoklum

Я думаю, что проблема здесь связана с отсутствием использования беззнакового типа, у GCC всегда были проблемы со статическим отбрасыванием поправки на переполнение и преобразование типа в подписанном / беззнаковом гибридном. Результат битового сдвига здесь является intрезультатом битовой операции МОЖЕТ БЫТЬ intИЛИ может long longзависеть от значения и формально enumне является эквивалентом intконстанты. clang требует «как будто», gcc остается педантичным
— Swift - Friday Pie

Ответы:


112

Лучшая версия :

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;
}

обратно в , мы можем проделать этот странный трюк:

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;
}

или, если мы застряли с , мы можем решить ее рекурсивно:

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...);
}

Godbolt со всеми 3 - вы можете переключить CPP_VERSION define, и получить идентичную сборку.

На практике я бы использовал самое современное, что мог. 14 лучше 11, потому что у нас нет рекурсии и, следовательно, длина символа O (n ^ 2) (что может резко увеличить время компиляции и использование памяти компилятора); 17 лучше 14, потому что компилятору не нужно удалять этот массив мертвым кодом, и этот трюк с массивом просто уродлив.

Из этих 14 самых запутанных. Здесь мы создаем анонимный массив всех нулей, тем временем в качестве побочного эффекта создаем наш результат, а затем отбрасываем массив. В отброшенном массиве есть количество нулей, равное размеру нашего пакета, плюс 1 (который мы добавляем, чтобы мы могли обрабатывать пустые пакеты).


Подробное объяснение того, что версия делает. Это уловка / хакерство, и тот факт, что вы должны сделать это для эффективного расширения пакетов параметров в C ++ 14, является одной из причин, почему выражения сворачивания были добавлены в.

Лучше всего это понять изнутри:

    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 0s, который также устанавливает биты в rкачестве побочного эффекта. Затем мы назначаем {}массиву инструкции по построению массива discard.

Далее мы приводим discardк void- большинство компиляторов предупредит вас, если вы создадите переменную и никогда ее не прочитаете. Все компиляторы не будут жаловаться, если вы его приведете void, это своего рода способ сказать «Да, я знаю, я не использую это», поэтому предупреждение подавляется.


38
Извините, но этот код C ++ 14 - это что-то. Не знаю что.
— Джеймс

14
@James Это прекрасный мотивирующий пример того, почему выражения свёртки в C ++ 17 очень приветствуются. Это и аналогичные уловки оказались эффективным способом расширения пакета «на месте» без какой-либо рекурсии, и компиляторы считают, что его легко оптимизировать.
— Якк - Адам Неврамонт

4
@ruben multi line constexpr является незаконным в 11
— Yakk - Адам Неврамонт

6
Я не вижу себя проверяющим этот код C ++ 14. Я буду придерживаться кода C ++ 11, так как он мне все равно нужен, но даже если бы я мог его использовать, код C ++ 14 потребовал бы так много объяснений, что я бы не стал. Эти маски всегда могут содержать не более 32 элементов, поэтому меня не беспокоит поведение O (n ^ 2). В конце концов, если n ограничено константой, то это действительно O (1). ;)
— Alex Reinking

9
Для тех, кто пытается понять, ((1ull<<indexes)|...|0ull)это «складное выражение» . В частности, это «двоичная правая складка», и ее следует анализировать как(pack op ... op init)
— Хенрик Хансен

47

Оптимизация, которую вы ищете, похоже, связана с отслаиванием петель, которое включается -O3или вручную -fpeel-loops. Я не уверен, почему это подпадает под действие отслаивания цикла, а не развертывания цикла, но, возможно, он не хочет развернуть цикл с нелокальным потоком управления внутри него (как, возможно, есть из проверки диапазона).

Однако по умолчанию GCC не может очистить все итерации, что, по-видимому, необходимо. Экспериментально, передача -O2 -fpeel-loops --param max-peeled-insns=200(значение по умолчанию 100) выполняет работу с вашим исходным кодом: https://godbolt.org/z/NNWrga


Вы великолепны, спасибо! Я понятия не имел, что это можно настроить в GCC! Хотя почему-то -O3 -fpeel-loops --param max-peeled-insns=200не получается ... Это -ftree-slp-vectorizeвидимо из-за .
— Alex Reinking

Это решение, похоже, ограничено целью x86-64. Результат для ARM и ARM64 все еще не очень хорош, что опять же может быть совершенно неактуальным для OP.
— Realtime

@realtime - это вообще-то актуально. Спасибо, что указали, что в этом случае это не работает. Очень досадно, что GCC не улавливает его до того, как будет понижен до IR для конкретной платформы. LLVM оптимизирует его перед дальнейшим понижением.
— Alex Reinking

10

если использование только 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 ответ yakk уже прекрасно охватывает это.


3
Как это демонстрирует, что на apply_known_maskсамом деле оптимизирует?
— Alex Reinking

2
@AlexReinking: Все страшные вещи constexpr. И хотя этого теоретически недостаточно, мы знаем, что GCC вполне способен выполнять оценку, constexprкак задумано.
— MSalters

8

Я бы посоветовал вам написать правильный EnumSetшрифт.

Написание базового кода EnumSet<E>на C ++ 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;
}

В C ++ 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;
}

Даже GCC банально генерирует andинструкцию на -O1 Godbolt :

apply_known_mask(EnumSet<Flags>&):
        and     QWORD PTR [rdi], 775946532
        ret

2
В С ++ 11 большая часть вашего constexprкода незаконна. Я имею ввиду, у некоторых есть 2 утверждения! (C ++ 11 constexpr отстойный)
— Якк - Адам Неврамонт

@ Yakk-AdamNevraumont: Вы понимали, что я опубликовал 2 версии кода: первую для C ++ 14 и далее, а вторую специально для C ++ 11? (чтобы учесть его ограничения)
— Matthieu M.

1
Возможно, лучше использовать std :: under_type вместо std :: uint64_t.
— Джеймс

@ Джеймс: Вообще-то нет. Обратите внимание, что EnumSet<E>не использует значение Eas напрямую, а вместо этого использует 1 << e. Это совершенно другой домен, что на самом деле делает класс таким ценным => нет шансов случайно проиндексировать eвместо 1 << e.
— Matthieu M.

@MatthieuM. Да, ты прав. Я путаю это с нашей собственной реализацией, которая очень похожа на вашу. Недостаток использования (1 << e) заключается в том, что если e выходит за пределы размера under_type, то это, вероятно, UB, надеюсь, ошибка компилятора.
— Джеймс

7

Начиная с C ++ 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 заключается в том, что он потенциально немного быстрее компилируется из-за правила Chiel .


1

Здесь есть несколько далеких от "умных" идей. Вы, вероятно, не улучшите ремонтопригодность, следуя им.

является

{B, D, E, H, K, M, L, O};

намного проще написать чем

(B| D| E| H| K| M| L| O);

?

Тогда никакой остальной код не нужен.


1
«B», «D» и т. Д. Сами по себе не являются флагами.
— Michał Łoś

Да, вам нужно сначала преобразовать их во флаги. В моем ответе это совсем не ясно. Прости. Буду обновлять.
— ANone
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.