web-dev-qa-db-fra.com

Comment puis-je écrire un masque de bits maintenable, rapide et à la compilation en C ++?

J'ai un code qui ressemble plus ou moins à ceci:

#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 fait la chose intelligente et la compile en une seule instruction and (qui est ensuite alignée partout ailleurs):

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

Mais chaque version de GCC que j'ai essayée compile cela en un énorme gâchis qui inclut la gestion des erreurs qui devrait être statiquement DCE. Dans un autre code, il placera même le important_bits équivalent comme donnée conforme au code!

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

Comment dois-je écrire ce code pour que les deux compilateurs puissent faire la bonne chose? A défaut, comment dois-je écrire ceci pour qu'il reste clair, rapide et maintenable?

113
Alex Reinking

La meilleure version est c ++ 17 :

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

Ensuite

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

de retour dans c ++ 14 , nous pouvons faire cette étrange astuce:

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

ou, si nous sommes coincés avec c ++ 11 , nous pouvons le résoudre récursivement:

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 avec les - vous pouvez changer de définition CPP_VERSION et obtenir un assemblage identique.

En pratique, j'utiliserais le plus moderne possible. 14 bat 11 parce que nous n'avons pas de récursivité et donc de longueur de symbole O (n ^ 2) (ce qui peut exploser le temps de compilation et l'utilisation de la mémoire du compilateur); 17 bat 14 parce que le compilateur n'a pas à éliminer de code mort ce tableau, et cette astuce de tableau est tout simplement moche.

De ces 14, c'est le plus déroutant. Ici, nous créons un tableau anonyme de tous les 0, en attendant comme effet secondaire, construisons notre résultat, puis jetons le tableau. Le tableau mis au rebut contient un nombre de 0 égal à la taille de notre pack, plus 1 (que nous ajoutons pour pouvoir gérer les packs vides).


Une explication détaillée de ce que fait la version c ++ 14 . C'est une astuce/hack, et le fait que vous deviez le faire pour étendre les packs de paramètres avec efficacité en C++ 14 est l'une des raisons pour lesquelles les expressions de repli ont été ajoutées dans c ++ 17 .

Il est mieux compris de l'intérieur:

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

cela met simplement à jour r avec 1<<indexes pour un index fixe. indexes est un pack de paramètres, nous devrons donc l'étendre.

Le reste du travail consiste à fournir un pack de paramètres pour développer indexes à l'intérieur de.

Un pas en avant:

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

ici, nous convertissons notre expression en void, indiquant que nous ne nous soucions pas de sa valeur de retour (nous voulons juste l'effet secondaire de la définition de r - en C++, des expressions comme a |= b renvoie également la valeur qu'ils ont définie a sur).

Ensuite, nous utilisons l'opérateur virgule , et 0 pour supprimer la void "valeur" et renvoyer la valeur 0. Il s'agit donc d'une expression dont la valeur est 0 et comme effet secondaire du calcul de 0, elle définit un bit dans r.

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

À ce stade, nous développons le pack de paramètres indexes. Nous obtenons donc:

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

dans le {}. Cette utilisation de , n'est pas l'opérateur virgule, mais plutôt le séparateur des éléments du tableau. Il s'agit de sizeof...(indexes)+10 s, qui définissent également des bits dans r comme effet secondaire. Nous affectons ensuite les instructions de construction du tableau {} à un tableau discard.

Ensuite, nous convertissons discard en void - la plupart des compilateurs vous avertiront si vous créez une variable et ne la lisez jamais. Tous les compilateurs ne se plaindront pas si vous le transformez en void, c'est en quelque sorte une façon de dire "Oui, je sais, je ne l'utilise pas", donc cela supprime l'avertissement.

112

L'optimisation que vous recherchez semble être le décollement de boucle, qui est activé à -O3, ou manuellement avec -fpeel-loops. Je ne sais pas pourquoi cela relève du pelage de boucle plutôt que du déroulement de la boucle, mais il n'est peut-être pas disposé à dérouler une boucle avec un flux de contrôle non local à l'intérieur (comme il y a, potentiellement, à partir de la vérification de la plage).

Par défaut, cependant, GCC ne peut pas décortiquer toutes les itérations, ce qui est apparemment nécessaire. Expérimentalement, en passant -O2 -fpeel-loops --param max-peeled-insns=200 (la valeur par défaut est 100) fait le travail avec votre code d'origine: https://godbolt.org/z/NNWrga

47
Sneftel

si vous utilisez uniquement C++ 11 est un must (&a)[N] est un moyen de capturer des tableaux. Cela vous permet d'écrire une seule fonction récursive sans utiliser de fonctions d'assistance:

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

l'assigner à un 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;
}

Tester

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

Production

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

il faut vraiment apprécier la capacité de C++ à calculer tout ce qui est calculable au moment de la compilation. Cela me souffle sûrement encore l'esprit ( <> ).


Pour les versions ultérieures C++ 14 et C++ 17 yakk's réponse couvre déjà à merveille cela.

10
Stack Danny

Je vous encourage à écrire un type EnumSet approprié.

Écrire une base EnumSet<E> en C++ 14 (à partir de) basé sur std::uint64_t est trivial:

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

Cela vous permet d'écrire du code simple:

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

    flags &= IMPORTANT;
}

En C++ 11, il nécessite quelques circonvolutions, mais reste néanmoins possible:

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

Et est invoqué avec:

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

Même GCC génère trivialement une instruction and à -O1godbolt :

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

Depuis C++ 11, vous pouvez également utiliser la technique TMP classique:

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

Lien vers l'explorateur du compilateur: https://godbolt.org/z/Gk6KX1

L'avantage de cette approche sur la fonction constexpr du modèle est qu'elle est potentiellement légèrement plus rapide à compiler en raison de règle de Chiel .

7
Michał Łoś

Il y a ici des idées très "intelligentes". Vous ne contribuez probablement pas à la maintenabilité en les suivant.

est

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

tellement plus facile à écrire que

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

?

Ensuite, aucun du reste du code n'est nécessaire.

1
ANone