Jak napisać łatwą w utrzymaniu, szybką i kompilującą maskę bitową w C ++?

113

Mam kod mniej więcej taki:

#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 robi inteligentną rzecz i kompiluje to do pojedynczej andinstrukcji (która jest następnie wstawiana wszędzie indziej):

apply_known_mask(std::bitset<64ul>&):  # @apply_known_mask(std::bitset<64ul>&)
        and     qword ptr [rdi], 775946532
        ret

Ale każda wersja GCC, którą próbowałem, kompiluje to do ogromnego bałaganu, który obejmuje obsługę błędów, która powinna być statycznie DCE. W innym kodzie nawet umieści important_bitsodpowiednik jako dane zgodnie z kodem!

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

Jak napisać ten kod, aby oba kompilatory działały właściwie? W przeciwnym razie jak mam to napisać, aby było jasne, szybkie i możliwe do utrzymania?

Alex Reinking
źródło
4
Zamiast używać pętli, czy nie możesz skonstruować maski za pomocą B | D | E | ... | O?
HolyBlackCat
6
Wyliczenie ma raczej pozycje bitów niż już rozwinięte bity, więc mogę to zrobić(1ULL << B) | ... | (1ULL << O)
Alex Reinking
3
Wadą jest to, że rzeczywiste nazwy są długie i nieregularne i nie jest tak łatwo zobaczyć, które flagi są w masce z całym tym szumem linii.
Alex Reinking
4
@AlexReinking Możesz to zrobić (1ULL << Constant)| w każdym wierszu i wyrównaj stałe nazwy w różnych wierszach, co będzie łatwiejsze dla oczu.
einpoklum
Myślę, że problem tutaj związany z brakiem użycia typu unsigned, GCC zawsze miał problemy ze statycznym odrzucaniem korekcji przepełnienia i konwersji typu w hybrydzie ze znakiem / bez znaku Wynik przesunięcia bitów tutaj jest intwynikiem operacji na bitach MOŻE BYĆ intLUB może long longzależeć od wartości i formalnie enumnie jest odpowiednikiem intstałej. clang woła o „jak gdyby”, gcc pozostaje pedantyczny
Swift - Friday Pie

Odpowiedzi:

112

Najlepsza wersja to :

template< unsigned char... indexes >
constexpr unsigned long long mask(){
  return ((1ull<<indexes)|...|0ull);
}

Następnie

void apply_known_mask(std::bitset<64> &bits) {
  constexpr auto m = mask<B,D,E,H,K,M,L,O>();
  bits &= m;
}

z powrotem w , możemy zrobić tę dziwną sztuczkę:

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

lub, jeśli utknęliśmy możemy to rozwiązać rekurencyjnie:

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 ze wszystkimi 3 - możesz przełączyć CPP_VERSION zdefiniować i uzyskać identyczny montaż.

W praktyce użyłbym najnowocześniejszego, jak tylko potrafię. 14 pokonuje 11, ponieważ nie mamy rekursji, a zatem O (n ^ 2) długości symbolu (co może spowodować eksplozję czasu kompilacji i wykorzystania pamięci kompilatora); 17 bije 14, ponieważ kompilator nie musi usuwać tej tablicy martwego kodu, a ta sztuczka tablicowa jest po prostu brzydka.

Spośród nich 14 jest najbardziej zagmatwanych. Tutaj tworzymy anonimową tablicę wszystkich zer, tymczasem jako efekt uboczny konstruujemy nasz wynik, a następnie odrzucamy tablicę. Wyrzucona tablica zawiera liczbę 0 równą rozmiarowi naszej paczki, plus 1 (którą dodajemy, abyśmy mogli obsłużyć puste paczki).


Szczegółowe wyjaśnienie, co jest wersja robi. To jest sztuczka / hack, a fakt, że musisz to zrobić, aby rozszerzyć pakiety parametrów z wydajnością w C ++ 14, jest jednym z powodów, dla których wyrażenia fold zostały dodane w.

Najlepiej to zrozumieć od podszewki:

    r |= (1ull << indexes) // side effect, used

to właśnie aktualizuje rsię 1<<indexesza ustaloną indeksu. indexesjest pakietem parametrów, więc będziemy musieli go rozszerzyć.

Reszta pracy polega na dostarczeniu pakietu parametrów do rozszerzenia indexeswewnątrz.

Jeden krok do przodu:

(void(
    r |= (1ull << indexes) // side effect, used
  ),0)

tutaj rzutujemy nasze wyrażenie na void, co oznacza, że ​​nie obchodzi nas jego wartość zwracana (chcemy tylko efektu ubocznego ustawienia r- w C ++ wyrażenia takie jak a |= brównież zwracają wartość, którą ustawili a).

Następnie używamy operatora przecinka ,i 0odrzucamy void„wartość”, a zwracamy wartość 0. Więc to jest wyrażenie, którego wartość jest 0i jako efekt uboczny obliczania 0, ustawia ją nieco r.

  int discard[] = {0,(void(
    r |= (1ull << indexes) // side effect, used
  ),0)...};

W tym momencie rozszerzamy pakiet parametrów indexes. Więc otrzymujemy:

 {
    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),
  }

w {}. To użycie nie, jest operatorem przecinka, ale raczej separatorem elementu tablicy. To jest s, które również ustawia bity jako efekt uboczny. Następnie przypisujemy instrukcje konstrukcji tablicy do tablicy .sizeof...(indexes)+1 0r{}discard

Następnie rzucamy discardna void- większość kompilatorów ostrzeże Cię, jeśli utworzysz zmienną i nigdy jej nie przeczytasz. Wszyscy kompilatorzy nie będą narzekać, jeśli rzucisz to na void, jest to rodzaj powiedzenia „Tak, wiem, nie używam tego”, więc to pomija ostrzeżenie.

Yakk - Adam Nevraumont
źródło
38
Przepraszamy, ale ten kod w C ++ 14 to coś. Nie wiem co.
James
14
@James To wspaniały motywujący przykład, dlaczego wyrażenia fold w C ++ 17 są bardzo mile widziane. To i podobne sztuczki okazują się skutecznym sposobem rozszerzania pakietu „w miejscu” bez żadnej rekursji, a osoby obsługujące są łatwe do optymalizacji.
Yakk - Adam Nevraumont
4
@ruben multi line constexpr jest nielegalne w 11
Yakk - Adam Nevraumont
6
Nie widzę siebie, jak sprawdzam ten kod C ++ 14. Zostanę przy C ++ 11, ponieważ i tak go potrzebuję, ale nawet gdybym mógł go użyć, kod C ++ 14 wymaga tylu wyjaśnień, których bym nie zrobił. Te maski można zawsze napisać tak, aby zawierały maksymalnie 32 elementy, więc nie martwię się zachowaniem O (n ^ 2). W końcu, jeśli n jest ograniczone przez stałą, to tak naprawdę jest O (1). ;)
Alex Reinking
9
Dla tych, którzy próbują to zrozumieć ((1ull<<indexes)|...|0ull), jest to „fałdowe wyrażenie” . W szczególności jest to „binarne prawe zagięcie” i powinno być analizowane jako(pack op ... op init)
Henrik Hansen,
47

Optymalizacja, której szukasz, wydaje się być peelingiem w pętli, który jest włączony -O3lub ręcznie za pomocą -fpeel-loops. Nie jestem pewien, dlaczego należy to do zakresu peelingu pętli, a nie rozwijania pętli, ale prawdopodobnie nie chce rozwinąć pętli z nielokalnym przepływem kontrolnym w niej (jak to jest, potencjalnie, z kontroli zasięgu).

Domyślnie jednak GCC przestaje być w stanie usunąć wszystkie iteracje, co najwyraźniej jest konieczne. Eksperymentalnie przekazanie -O2 -fpeel-loops --param max-peeled-insns=200(wartość domyślna to 100) powoduje wykonanie zadania przy użyciu oryginalnego kodu: https://godbolt.org/z/NNWrga

Sneftel
źródło
Jesteś niesamowity, dziękuję! Nie miałem pojęcia, że ​​można to skonfigurować w GCC! Chociaż z jakiegoś powodu -O3 -fpeel-loops --param max-peeled-insns=200zawodzi ... To -ftree-slp-vectorizenajwyraźniej przez.
Alex Reinking
To rozwiązanie wydaje się być ograniczone do celu x86-64. Wynik dla ARM i ARM64 nadal nie jest ładny, co z kolei może być zupełnie nieistotne dla OP.
czasie rzeczywistym
@realtime - właściwie jest to dość istotne. Dziękuję za wskazanie, że to nie działa w tym przypadku. Bardzo rozczarowujące, że GCC nie łapie go przed obniżeniem do IR specyficznej dla platformy. LLVM optymalizuje go przed dalszym obniżaniem.
Alex Reinking
10

jeśli używanie tylko C ++ 11 jest koniecznością, (&a)[N]jest sposobem na przechwytywanie tablic. Pozwala to na napisanie jednej funkcji rekurencyjnej bez korzystania z funkcji pomocniczych:

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

przypisanie go do 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;
}

Test

int main() {
    std::bitset<64> b;
    b.flip();
    apply_known_mask(b);
    std::cout << b.to_string() << '\n';
}

Wynik

0000000000000000000000000000000000101110010000000000000100100100
//                                ^ ^^^  ^             ^  ^  ^
//                                O MLK  H             E  D  B

naprawdę trzeba docenić zdolność C ++ do obliczania wszystkiego, co można obliczyć w czasie kompilacji. To na pewno nadal mi się podoba ( <> ).


W przypadku nowszych wersji C ++ 14 i C ++ 17 odpowiedź Yakka już wspaniale to pokrywa.

Stack Danny
źródło
3
Jak to pokazuje, że apply_known_maskfaktycznie optymalizuje?
Alex Reinking
2
@AlexReinking: wszystkie przerażające bity są constexpr. I chociaż to teoretycznie nie wystarczy, wiemy, że GCC jest w stanie ocenić constexprzgodnie z zamierzeniami.
MSalters
8

Zachęcam do napisania odpowiedniego EnumSettypu.

Pisanie podstawowego EnumSet<E>języka w C ++ 14 (wzwyż) na podstawie std::uint64_tjest trywialne:

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

Pozwala to napisać prosty kod:

void apply_known_mask(EnumSet<Flags>& flags) {
    static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };

    flags &= IMPORTANT;
}

W C ++ 11 wymaga pewnych zwojów, ale mimo to pozostaje możliwe:

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

I jest wywoływany przez:

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

Nawet GCC w trywialny sposób generuje andinstrukcję w -O1 godbolt :

apply_known_mask(EnumSet<Flags>&):
        and     QWORD PTR [rdi], 775946532
        ret
Matthieu M.
źródło
2
W C ++ 11 znaczna część twojego constexprkodu nie jest legalna. Mam na myśli, że niektóre mają 2 stwierdzenia! (C ++ 11 constexpr do dupy)
Yakk - Adam Nevraumont
@ Yakk-AdamNevraumont: Czy zdajesz sobie sprawę, że opublikowałem 2 wersje kodu, pierwszą dla C ++ 14 i nowszą, a drugą specjalnie dostosowaną do C ++ 11? (aby uwzględnić jego ograniczenia)
Matthieu M.,
1
Lepszym rozwiązaniem może być użycie std :: Base_type zamiast std :: uint64_t.
James,
@James: Właściwie nie. Zwróć uwagę, że EnumSet<E>nie używa wartości Eas bezpośrednio, ale zamiast tego używa 1 << e. To zupełnie inna domena, co w rzeczywistości sprawia, że ​​klasa jest tak cenna => nie ma szans na przypadkowe indeksowanie przez ezamiast 1 << e.
Matthieu M.
@MatthieuM. Tak, masz rację. Mylę to z naszą własną implementacją, która jest bardzo podobna do Twojej. Wadą używania (1 << e) jest to, że jeśli e jest poza zakresem dla rozmiaru podstawowego_typu, to prawdopodobnie jest to UB, miejmy nadzieję, że jest to błąd kompilatora.
James
7

Od C ++ 11 możesz również użyć klasycznej techniki 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;
}

Link do Compiler Explorer: https://godbolt.org/z/Gk6KX1

Zaletą tego podejścia w porównaniu z funkcją constexpr szablonu jest to, że kompilacja jest potencjalnie nieco szybsza z powodu reguły Chiel .

Michał Łoś
źródło
1

Jest tu sporo do „sprytnych” pomysłów. Prawdopodobnie nie pomagasz w utrzymaniu, przestrzegając ich.

jest

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

o wiele łatwiejsze do napisania niż

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

?

Wtedy żadna reszta kodu nie jest potrzebna.

Ktoś
źródło
1
„B”, „D” itd. Same w sobie nie są flagami.
Michał Łoś
Tak, musisz najpierw przekształcić je w flagi. W mojej odpowiedzi wcale nie jest to jasne. Przepraszam. Zaktualizuję.
ANone