Як я можу записати ремонтовану, швидку бітову маску, що склалася, на C ++?


113

У мене є якийсь код, який більш-менш схожий на цей:

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

Як мені написати цей код, щоб обидва компілятори могли зробити правильно? Якщо цього не зробити, то як мені це написати, щоб воно залишалося зрозумілим, швидким та досяжним?


4
Замість того, щоб використовувати цикл, ви не можете створити маску B | D | E | ... | O?
HolyBlackCat

6
Перерахунок має бітові позиції, а не вже розширені біти, тому я міг би зробити(1ULL << B) | ... | (1ULL << O)
Алекс

3
Мінусом є те, що фактичні назви довгі та нерегулярні, і не так просто зрозуміти, які прапори є у масці при всьому шумі цього рядка.
Алекс

4
@AlexReinking Ви можете зробити це одним (1ULL << Constant)| на рядок і вирівняйте постійні назви в різних лініях, що буде простіше для очей.
einpoklum

Я думаю, що тут проблема, пов’язана з відсутністю непідписаного типу, GCC завжди мав проблеми зі стаціонарним відхиленням корекції для переповнення та перетворення типу в підписаний / непідписаний гібрид. Результат бітового зсуву тут є intрезультатом бітової операції МОЖЕ intАБО може бути long longзалежно від значення і формально enumце не еквівалент intконстанті. кланг закликає "ніби", gcc залишається педантичним
Свіфт - П'ятниця Пиріг

Відповіді:


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 і отримати однакову збірку.

На практиці я використовував би найсучасніше, що міг. 14 ударів 11, оскільки у нас немає рекурсії, а отже, і довжина символу O (n ^ 2) (яка може вибухнути час компіляції та використання пам'яті компілятора); 17 ударів 14, тому що компілятору не доведеться ліквідувати цей масив, а цей трюк масиву просто некрасивий.

З них 14 найбільш заплутаних. Тут ми створюємо анонімний масив усіх 0, тим часом як побічний ефект конструюємо наш результат, а потім відкидаємо масив. Відкинутий масив має в ньому число 0, рівне розміру нашої упаковки, плюс 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
Як - Адам Невраумон

6
Я не бачу себе перевіряти код C ++ 14. Я дотримуватимусь C ++ 11, оскільки мені це потрібно, все одно, але навіть якби я міг його використовувати, код C ++ 14 вимагає стільки пояснень, я б не став. Ці маски завжди можуть бути записані таким чином, що мають максимум 32 елементи, тому я не переживаю за поведінку O (n ^ 2). Зрештою, якщо n обмежений постійною, то це дійсно O (1). ;)
Олексій

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

Здається, це рішення обмежене ціллю x86-64. Вихідні дані для ARM та ARM64 все ще не дуже гарні, що потім може бути абсолютно не має значення для ОП.
режимі реального часу

@realtime - насправді це дещо актуально. Дякуємо, що вказали, що це не працює в цьому випадку. Дуже розчаровує те, що GCC не сприймає його перед тим, як опустити його на ІР-платформу. LLVM оптимізує його перед подальшим зниженням.
Алекс

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 відповідь якка вже чудово охоплює це.


3
Як це демонструє, що apply_known_maskнасправді оптимізує?
Алекс

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
У c ++ 11 значна частина вашого constexprкоду не є законною. Я маю на увазі, у деяких є 2 твердження! (C ++ 11 смоктав контекспр)
Як - Адам Невраумон

@ Yakk-AdamNevraumont: Ви зрозуміли, що я опублікував 2 версії коду, перша для C ++ 14 і друга, спеціально розроблена для C ++ 11? (для обліку обмежень)
Матьє М.

1
Можливо, краще використовувати std :: underlying_type замість std :: uint64_t.
Джеймс

@James: Насправді, ні. Зверніть увагу, що EnumSet<E>не використовується значення Eяк значення безпосередньо, а натомість використовує 1 << e. Це зовсім інший домен, який насправді робить клас таким цінним => немає шансів випадково проіндексувати його eзамість 1 << e.
Матьє М.

@MatthieuM. Так, ви праві. Я плутаю це з нашою власною реалізацією, яка дуже схожа на вашу. Недоліком використання (1 << e) є те, що якщо e не виходить за межі розміру underlying_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;
}

Посилання на провідник компілятора: 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.