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

113

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 compile cela en une seule andinstruction (qui est ensuite intégré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 dans un énorme désordre qui inclut la gestion des erreurs qui devraient être statiquement DCE. Dans un autre code, il placera même l' important_bitséquivalent sous forme de données en ligne avec le 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 écrire ce code pour que les deux compilateurs puissent faire la bonne chose? A défaut, comment écrire ceci pour qu'il reste clair, rapide et maintenable?

Alex Reinking
la source
4
Plutôt que d'utiliser une boucle, ne pouvez-vous pas construire un masque avec B | D | E | ... | O?
HolyBlackCat
6
L'énumération a des positions de bits plutôt que des bits déjà développés, donc je pourrais le faire(1ULL << B) | ... | (1ULL << O)
Alex Reinking
3
L'inconvénient est que les noms réels sont longs et irréguliers et qu'il n'est pas aussi facile de voir quels drapeaux se trouvent dans le masque avec tout ce bruit de ligne.
Alex Reinking le
4
@AlexReinking Vous pourriez en faire un (1ULL << Constant)| par ligne, et alignez les noms de constantes sur les différentes lignes, ce serait plus facile pour les yeux.
einpoklum
Je pense que le problème ici est lié au manque d'utilisation de type non signé, GCC a toujours eu des problèmes avec la correction de rejet statique pour le débordement et la conversion de type dans l'hybride signé / non signé.Le résultat du décalage de bit ici est le intrésultat de l'opération de bit PEUT ÊTRE intOU peut long longdépendre de la valeur et enumn'est formellement pas un équivalent à une intconstante. clang appelle à "comme si", gcc reste pédant
Swift - Friday Pie

Réponses:

112

La meilleure version est :

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 , 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 , 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 3 - vous pouvez changer CPP_VERSION define et obtenir un assemblage identique.

En pratique, j'utiliserais le plus moderne possible. 14 battements 11 parce que nous n'avons pas de récursion et donc de longueur de symbole O (n ^ 2) (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 le code mort de ce tableau, et cette astuce de tableau est tout simplement moche.

Parmi ceux-ci, 14 est le plus déroutant. Ici, nous créons un tableau anonyme de tous les 0, pendant ce temps, comme effet secondaire, construisons notre résultat, puis rejetons le tableau. Le tableau abandonné 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 la version fait. 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.

Il est mieux compris de l'intérieur:

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

cela se met juste à jour ravec 1<<indexespour un index fixe. indexesest un pack de paramètres, nous devrons donc l'étendre.

Le reste du travail consiste à fournir un pack de paramètres à développer à l' indexesinté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 r- en C ++, les expressions comme a |= bretournent également la valeur qu'elles ont définie a).

Ensuite, nous utilisons l'opérateur virgule ,et 0pour supprimer la void"valeur", et renvoyer la valeur 0. Donc, c'est une expression dont la valeur est 0et comme effet secondaire du calcul, 0elle s'installe un peu r.

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

À ce stade, nous développons le pack de paramètres indexes. On obtient 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 d'élément de tableau. C'est sizeof...(indexes)+1 0s, qui définit également les bits rcomme effet secondaire. Nous attribuons ensuite les {}instructions de construction du tableau à un tableau discard.

Ensuite, nous convertissons discarden 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 convertissez void, c'est en quelque sorte une façon de dire "Oui, je sais, je n'utilise pas ceci", donc cela supprime l'avertissement.

Yakk - Adam Nevraumont
la source
38
Désolé, mais ce code C ++ 14 est quelque chose. Je ne sais pas quoi.
James
14
@James C'est un merveilleux exemple motivant de la raison pour laquelle les expressions fold en C ++ 17 sont les bienvenues. Cela, et des astuces similaires, se révèlent être un moyen efficace d'étendre un pack "inplace" sans aucune récursivité et que les complices trouvent facile à optimiser.
Yakk - Adam Nevraumont
4
@ruben multi line constexpr est illégal en 11
Yakk - Adam Nevraumont
6
Je ne me vois pas enregistrer ce code C ++ 14. Je m'en tiendrai au C ++ 11 car j'en ai besoin de toute façon, mais même si je pouvais l'utiliser, le code C ++ 14 nécessite tellement d'explications que je ne le ferais pas. Ces masques peuvent toujours être écrits pour avoir au plus 32 éléments, donc je ne suis pas inquiet du comportement O (n ^ 2). Après tout, si n est borné par une constante, alors c'est vraiment O (1). ;)
Alex Reinking le
9
Pour ceux qui essaient de comprendre, ((1ull<<indexes)|...|0ull)c'est une «expression de pli» . Plus précisément, il s'agit d'un «pli droit binaire» et il devrait être analysé comme(pack op ... op init)
Henrik Hansen
47

L'optimisation que vous recherchez semble être le peeling de boucle, qui est activé à -O3ou manuellement avec -fpeel-loops. Je ne sais pas pourquoi cela relève du pelage de la boucle plutôt que du déroulement de la boucle, mais il est peut-être réticent à dérouler une boucle avec un flux de contrôle non local à l'intérieur (car il existe, potentiellement, du contrôle de plage).

Par défaut, cependant, GCC ne parvient pas à éplucher toutes les itérations, ce qui est apparemment nécessaire. Expérimentalement, passer -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

Sneftel
la source
Vous êtes incroyable merci! Je n'avais aucune idée que c'était configurable dans GCC! Bien que pour une raison quelconque -O3 -fpeel-loops --param max-peeled-insns=200échoue ... C'est dû -ftree-slp-vectorizeapparemment.
Alex Reinking le
Cette solution semble se limiter à la cible x86-64. La sortie pour ARM et ARM64 n'est toujours pas jolie, ce qui pourrait à nouveau être complètement hors de propos pour OP.
temps réel du
@realtime - c'est quelque peu pertinent, en fait. Merci de souligner que cela ne fonctionne pas dans ce cas. Très décevant que GCC ne l'attrape pas avant d'être abaissé à un IR spécifique à la plate-forme. LLVM l'optimise avant toute nouvelle descente.
Alex Reinking le
10

si utiliser uniquement C ++ 11 est un must, (&a)[N]c'est un moyen de capturer des tableaux. Cela vous permet d'écrire une seule fonction récursive sans utiliser aucune fonction 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'attribuer à 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 ( <> ).


Pour les versions ultérieures, C ++ 14 et C ++ 17, la réponse de yakk couvre déjà parfaitement cela.

Empiler Danny
la source
3
Comment cela montre-t-il que cela apply_known_maskoptimise réellement?
Alex Reinking le
2
@AlexReinking: Tous les bits effrayants le sont constexpr. Et bien que cela ne soit théoriquement pas suffisant, nous savons que GCC est tout à fait capable d'évaluer constexprcomme prévu.
MSalters
8

Je vous encourage à écrire un EnumSettype approprié .

Écrire un élément EnumSet<E>de base en C ++ 14 (à partir de) basé sur std::uint64_test 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, cela nécessite quelques convolutions, 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 andinstruction chez -O1 godbolt :

apply_known_mask(EnumSet<Flags>&):
        and     QWORD PTR [rdi], 775946532
        ret
Matthieu M.
la source
2
En C ++ 11, une grande partie de votre constexprcode n'est pas légale. Je veux dire, certains ont 2 déclarations! (C ++ 11 constexpr sucé)
Yakk - Adam Nevraumont
@ Yakk-AdamNevraumont: Vous avez réalisé que j'avais publié 2 versions du code, la première pour C ++ 14 et une seconde spécialement conçue pour C ++ 11? (pour tenir compte de ses limites)
Matthieu M.
1
Il peut être préférable d'utiliser std :: sous-jacent_type au lieu de std :: uint64_t.
James
@James: En fait, non. Notez que EnumSet<E>n'utilise pas Edirectement une valeur de as value, mais utilise à la place 1 << e. C'est un domaine complètement différent, ce qui rend la classe si précieuse => aucune chance d'indexation accidentelle par eau lieu de 1 << e.
Matthieu M.
@MatthieuM. Oui tu as raison. Je le confond avec notre propre implémentation qui est très similaire à la vôtre. L'inconvénient d'utiliser (1 << e) est que si e est hors limites pour la taille du sous-jacent_type alors c'est probablement UB, espérons-le, une erreur du compilateur.
James
7

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 par rapport à la fonction modèle constexpr est qu'elle est potentiellement légèrement plus rapide à compiler en raison de la règle de Chiel .

Michał Łoś
la source
1

Il y a des idées loin d'être «intelligentes» ici. Vous n'aidez 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 des autres codes n'est nécessaire.

UN
la source
1
"B", "D", etc. ne sont pas eux-mêmes des drapeaux.
Michał Łoś
Oui, vous devez d'abord les transformer en indicateurs. Ce n'est pas du tout clair dans ma réponse. Désolé. Je mettrai à jour.
ANun