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 and
instruction (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?
c++
c++11
bit-manipulation
Alex Reinking
la source
la source
B | D | E | ... | O
?(1ULL << B) | ... | (1ULL << O)
(1ULL << Constant)
| par ligne, et alignez les noms de constantes sur les différentes lignes, ce serait plus facile pour les yeux.int
résultat de l'opération de bit PEUT ÊTREint
OU peutlong long
dépendre de la valeur etenum
n'est formellement pas un équivalent à uneint
constante. clang appelle à "comme si", gcc reste pédantRéponses:
La meilleure version est c ++ 17:
ensuite
de retour dans c ++ 14, nous pouvons faire cette étrange astuce:
ou, si nous sommes coincés avec c ++ 11, nous pouvons le résoudre récursivement:
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 c ++ 14la 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 dansc ++ 17.
Il est mieux compris de l'intérieur:
cela se met juste à jour
r
avec1<<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 à développer à l'
indexes
intérieur de.Un pas en avant:
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éfinitionr
- en C ++, les expressions commea |= b
retournent également la valeur qu'elles ont définiea
).Ensuite, nous utilisons l'opérateur virgule
,
et0
pour supprimer lavoid
"valeur", et renvoyer la valeur0
. Donc, c'est une expression dont la valeur est0
et comme effet secondaire du calcul,0
elle s'installe un peur
.À ce stade, nous développons le pack de paramètres
indexes
. On obtient donc:dans le
{}
. Cette utilisation de,
n'est pas l'opérateur virgule, mais plutôt le séparateur d'élément de tableau. C'estsizeof...(indexes)+1
0
s, qui définit également les bitsr
comme effet secondaire. Nous attribuons ensuite les{}
instructions de construction du tableau à un tableaudiscard
.Ensuite, nous convertissons
discard
envoid
- 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 convertissezvoid
, c'est en quelque sorte une façon de dire "Oui, je sais, je n'utilise pas ceci", donc cela supprime l'avertissement.la source
((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)
L'optimisation que vous recherchez semble être le peeling de boucle, qui est activé à
-O3
ou 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/NNWrgala source
-O3 -fpeel-loops --param max-peeled-insns=200
échoue ... C'est dû-ftree-slp-vectorize
apparemment.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:l'attribuer à un
constexpr auto
:Tester
Production
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.
la source
apply_known_mask
optimise réellement?constexpr
. Et bien que cela ne soit théoriquement pas suffisant, nous savons que GCC est tout à fait capable d'évaluerconstexpr
comme prévu.Je vous encourage à écrire un
EnumSet
type approprié .Écrire un élément
EnumSet<E>
de base en C ++ 14 (à partir de) basé surstd::uint64_t
est trivial:Cela vous permet d'écrire du code simple:
En C ++ 11, cela nécessite quelques convolutions, mais reste néanmoins possible:
Et est invoqué avec:
Même GCC génère trivialement une
and
instruction chez-O1
godbolt :la source
constexpr
code n'est pas légale. Je veux dire, certains ont 2 déclarations! (C ++ 11 constexpr sucé)EnumSet<E>
n'utilise pasE
directement une valeur de as value, mais utilise à la place1 << e
. C'est un domaine complètement différent, ce qui rend la classe si précieuse => aucune chance d'indexation accidentelle pare
au lieu de1 << e
.Depuis C ++ 11, vous pouvez également utiliser la technique TMP classique:
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 .
la source
Il y a des idées loin d'être «intelligentes» ici. Vous n'aidez probablement pas la maintenabilité en les suivant.
est
tellement plus facile à écrire que
?
Ensuite, aucun des autres codes n'est nécessaire.
la source