TL:DR Les compilateurs actuels ont toujours bool
des optimisations manquées en faisant des choses comme
(a&&b) ? x : y
. Mais la raison pour laquelle no qu'ils n'assument pas 0/1, ils sont juste nuls à ça.
De nombreuses utilisations de bool
sont destinés aux fonctions locales ou aux fonctions en ligne, de sorte que la booléisation vers une fonction 0
/ 1
peut optimiser et brancher (ou cmov ou autre) sur la condition originale. Il suffit de s'inquiéter de l'optimisation bool
les entrées/sorties lorsqu'elles doivent être transmises/renvoyées à travers quelque chose qui n'est pas en ligne, ou réellement stocké en mémoire.
Guide d'optimisation possible : combine bool
à partir de sources externes (arguments de fonction / mémoire) avec des opérateurs de type bitwise, comme a&b
. MSVC et ICC s'en sortent mieux. Je ne sais pas si c'est pire en local. bool
s. Attention à ce que a&b
est seulement équivalent à a&&b
para bool
et non des types entiers. 2 && 1
est vrai, mais 2 & 1
est 0, ce qui est faux. La fonction OU par bit n'a pas ce problème.
Je ne sais pas si cette directive sera un jour utile pour les locales qui ont été définies à partir d'une comparaison à l'intérieur de la fonction (ou dans quelque chose qui a été intégré). Par exemple, cela pourrait conduire le compilateur à créer des booléens entiers au lieu d'utiliser directement les résultats de la comparaison lorsque cela est possible. Notez également que cela ne semble pas aider avec les gcc et clang actuels.
Oui, les implémentations C++ sur x86 stockent bool
dans un octet qui est toujours 0 ou 1 (au moins à travers les frontières d'appel de fonction où le compilateur doit respecter l'ABI / convention d'appel qui l'exige).
Les compilateurs en tirent parfois parti, par exemple pour les éléments suivants bool
-> int
même gcc 4.4 se contente d'une extension zéro en 32 bits ( movzx eax, dil
). Clang et MSVC le font aussi. Les règles du C et du C++ exigent que cette conversion produise 0 ou 1, donc ce comportement n'est sûr que s'il s'agit de toujours on peut supposer qu'un bool
La fonction arg ou la variable globale a une valeur de 0 ou 1.
Même les anciens compilateurs en tiraient généralement parti pour bool
-> int
mais pas dans les autres cas. Ainsi, Agner se trompe sur la raison quand il dit :
La raison pour laquelle le compilateur ne fait pas une telle supposition est que les variables peuvent avoir d'autres valeurs si elles ne sont pas initialisées ou proviennent de sources inconnues.
MSVC CL19 fait du code qui assume bool
Les arguments de la fonction sont 0 ou 1, l'ABI de Windows x86-64 doit donc le garantir.
Dans le x86-64 System V ABI (utilisé par tout ce qui n'est pas Windows), le journal des modifications de la révision 0.98 indique "Spécifier que _Bool
(alias bool
) est booléenisé chez l'appelant". Je pense que même avant ce changement, les compilateurs le supposaient, mais ceci ne fait que documenter ce sur quoi les compilateurs s'appuyaient déjà. Le langage actuel dans le x86-64 SysV ABI est :
3.1.2 Représentation des données
Les booléens, lorsqu'ils sont stockés dans un objet mémoire, sont stockés sous forme d'objets à un seul octet dont la valeur est toujours 0 (faux) ou 1 (vrai). Lorsqu'ils sont stockés dans des registres entiers (sauf pour être passés en tant qu'arguments), les 8 octets du registre sont significatifs ; toute valeur non nulle est considérée comme vraie.
La deuxième phrase est absurde : l'ABI n'a pas à dire aux compilateurs comment stocker des choses dans des registres à l'intérieur d'une fonction, seulement aux frontières entre différentes unités de compilation (mémoire / args de fonction et valeurs de retour). J'ai signalé ce défaut de l'ABI il y a quelque temps sur la page github où il est maintenu .
3.2.3 Passage de paramètres :
Lorsqu'une valeur de type _Bool
est retourné ou transmis dans un registre ou sur la pile, le bit 0 contient la valeur de vérité et les bits 1 à 7 doivent être nuls. 16 .
(note de bas de page 16) : Les autres bits ne sont pas spécifiés, donc le côté consommateur de ces valeurs peut compter sur le fait qu'il s'agit de 0 ou de 1 lorsqu'elles sont tronquées à 8 bits.
Le langage de l'ABI du système V de l'i386 est le même, je crois.
Tout compilateur qui suppose 0/1 pour une chose (par exemple la conversion en int
) mais ne parvient pas à en tirer parti dans d'autres cas, a un optimisation manquée . Malheureusement, de telles optimisations manquées existent encore, bien qu'elles soient plus rares qu'à l'époque où Agner écrivait ce paragraphe sur les compilateurs toujours re-booleanisation.
(Source + asm sur le Explorateur de compilateur Godbolt pour gcc4.6 / 4.7, et clang/MSVC. Voir aussi la présentation de Matt Godbolt au CppCon2017. Qu'est-ce que mon compilateur a fait pour moi dernièrement ? Déboulonner le couvercle du compilateur )
bool logical_or(bool a, bool b) { return a||b; }
# gcc4.6.4 -O3 for the x86-64 System V ABI
test dil, dil # test a against itself (for non-zero)
mov eax, 1
cmove eax, esi # return a ? 1 : b;
ret
Donc même gcc4.6 n'a pas re-booléenisé b
mais il a manqué l'optimisation que gcc4.7 fait : (et clang et les compilateurs ultérieurs comme indiqué dans d'autres réponses) :
# gcc4.7 -O3 to present: looks ideal to me.
mov eax, esi
or eax, edi
ret
(Le or dil, sil
/ mov eax, edi
est stupide : il est garanti de provoquer un blocage partiel du registre sur Nehalem ou les versions antérieures d'Intel lors de la lecture des données. edi
après la rédaction dil
La taille du code est moins bonne car il faut un préfixe REX pour utiliser la partie low-8 d'edi. Un meilleur choix pourrait être or dil,sil
/ movzx eax, dil
si vous voulez éviter lecture tout registre 32 bits au cas où votre appelant aurait laissé des registres de passage d'arg avec des registres partiels "sales").
MSVC émet ce code qui vérifie a
puis b
séparément, sans tirer profit de quoi que ce soit et même en utilisant xor al,al
au lieu de xor eax,eax
. Il a donc une fausse dépendance vis-à-vis de l'ancienne valeur de eax
sur la plupart des CPU ( y compris Haswell/Skylake, qui ne renomme pas les registres partiels low-8 séparément du registre entier, seulement AH/BH/... ). C'est tout simplement stupide. La seule raison d'utiliser xor al,al
c'est lorsque vous voulez explicitement préserver les octets supérieurs.
logical_or PROC ; x86-64 MSVC CL19
test cl, cl ; Windows ABI passes args in ecx, edx
jne SHORT $LN3@logical_or
test dl, dl
jne SHORT $LN3@logical_or
xor al, al ; missed peephole: xor eax,eax is strictly better
ret 0
$LN3@logical_or:
mov al, 1
ret 0
logical_or ENDP
ICC18 ne tire pas non plus parti de la nature 0/1 connue des entrées, il utilise simplement une fonction or
pour positionner les drapeaux en fonction de la fonction OU bit à bit des deux entrées, et setcc
pour produire un 0/1.
logical_or(bool, bool): # ICC18
xor eax, eax #4.42
movzx edi, dil #4.33
movzx esi, sil #4.33
or edi, esi #4.42
setne al #4.42
ret #4.42
ICC émet le même code même pour bool bitwise_or(bool a, bool b) { return a|b; }
. Il encourage à int
(avec movzx
), et utilise or
pour mettre les drapeaux en fonction de l'OR bit à bit. Ceci est stupide comparé à or dil,sil
/ setne al
.
Pour bitwise_or
MSVC utilise simplement un or
instruction (après movzx
sur chaque entrée), mais de toute façon ne re-booleanise pas.
Optimisations manquées dans les gcc/clang actuels :
Seuls ICC/MSVC faisaient du code idiot avec la fonction simple ci-dessus, mais cette fonction donne toujours des problèmes à gcc et clang :
int select(bool a, bool b, int x, int y) {
return (a&&b) ? x : y;
}
Source+asm sur l'explorateur de compilateurs Godbolt (Même source, différents compilateurs sélectionnés par rapport à la dernière fois).
Cela semble assez simple ; on pourrait espérer qu'un compilateur intelligent le fasse sans branchement avec un seul fichier test
/ cmov
. x86 test
définit les drapeaux en fonction d'un ET par bit. C'est une instruction ET qui n'écrit pas réellement la destination. (Tout comme cmp
est un sub
qui n'écrit pas la destination).
# hand-written implementation that no compilers come close to making
select:
mov eax, edx # retval = x
test edi, esi # ZF = ((a & b) == 0)
cmovz eax, ecx # conditional move: return y if ZF is set
ret
Mais même les constructions quotidiennes de gcc et clang sur l'explorateur de compilateurs Godbolt font beaucoup un code plus compliqué, vérifiant chaque booléen séparément. Ils savent comment optimiser bool ab = a&&b;
si vous retournez ab
Mais même en l'écrivant de cette façon (avec une variable booléenne distincte pour contenir le résultat), on ne parvient pas à les convaincre de créer un code qui ne soit pas mauvais.
Notez que test same,same
est exactement équivalent à cmp reg, 0
et est plus petit, c'est donc ce que les compilateurs utilisent.
Clang est strictement pire que ma version écrite à la main. (Notez qu'elle exige que l'appelant ait étendu à zéro la fonction bool
args en 32 bits, comme il le fait pour les types d'entiers étroits comme une partie non officielle de l'ABI que lui et gcc implémentent mais dont seul clang dépend. ).
select: # clang 6.0 trunk 317877 nightly build on Godbolt
test esi, esi
cmove edx, ecx # x = b ? y : x
test edi, edi
cmove edx, ecx # x = a ? y : x
mov eax, edx # return x
ret
gcc 8.0.0 20171110 nightly fait du code de branchement pour cela, similaire à ce que font les anciennes versions de gcc.
select(bool, bool, int, int): # gcc 8.0.0-pre 20171110
test dil, dil
mov eax, edx ; compiling with -mtune=intel or -mtune=haswell would keep test/jcc together for macro-fusion.
je .L8
test sil, sil
je .L8
rep ret
.L8:
mov eax, ecx
ret
MSVC x86-64 CL19 fait un code très similaire à celui de Branchy. Il vise la convention d'appel de Windows, où les arguments entiers sont dans rcx, rdx, r8, r9.
select PROC
test cl, cl ; a
je SHORT $LN3@select
mov eax, r8d ; retval = x
test dl, dl ; b
jne SHORT $LN4@select
$LN3@select:
mov eax, r9d ; retval = y
$LN4@select:
ret 0 ; 0 means rsp += 0 after popping the return address, not C return 0.
; MSVC doesn't emit the `ret imm16` opcode here, so IDK why they put an explicit 0 as an operand.
select ENDP
ICC18 fait aussi du code branché, mais avec les deux mov
instructions après les branches.
select(bool, bool, int, int):
test dil, dil #8.13
je ..B4.4 # Prob 50% #8.13
test sil, sil #8.16
jne ..B4.5 # Prob 50% #8.16
..B4.4: # Preds ..B4.2 ..B4.1
mov edx, ecx #8.13
..B4.5: # Preds ..B4.2 ..B4.4
mov eax, edx #8.13
ret #8.13
Essayer d'aider le compilateur en utilisant
int select2(bool a, bool b, int x, int y) {
bool ab = a&&b;
return (ab) ? x : y;
}
conduit MSVC à faire du code hilarant et mauvais. :
;; MSVC CL19 -Ox = full optimization
select2 PROC
test cl, cl
je SHORT $LN3@select2
test dl, dl
je SHORT $LN3@select2
mov al, 1 ; ab = 1
test al, al ;; and then test/cmov on an immediate constant!!!
cmovne r9d, r8d
mov eax, r9d
ret 0
$LN3@select2:
xor al, al ;; ab = 0
test al, al ;; and then test/cmov on another path with known-constant condition.
cmovne r9d, r8d
mov eax, r9d
ret 0
select2 ENDP
C'est seulement avec MSVC (et ICC18 a la même optimisation manquée de test/cmov sur un registre qui vient d'être mis à une constante).
gcc et clang, comme d'habitude, ne font pas un code aussi mauvais que MSVC ; ils font le même asm qu'ils font pour select()
Ce n'est toujours pas bon, mais au moins, essayer de les aider n'aggrave pas la situation comme avec MSVC.
Combinez bool
avec les opérateurs bit à bit aide MSVC et ICC
Dans mes tests très limités, |
y &
semblent mieux fonctionner que ||
y &&
pour MSVC et ICC. Regardez la sortie du compilateur pour votre propre code avec votre compilateur + les options de compilation pour voir ce qui se passe.
int select_bitand(bool a, bool b, int x, int y) {
return (a&b) ? x : y;
}
Gcc se branche toujours séparément sur des sites distincts test
des deux entrées, même code que les autres versions de select
. clang fait toujours deux test/cmov
, même asm que pour les autres versions sources.
MSVC s'en sort et optimise correctement, battant tous les autres compilateurs (au moins dans la définition autonome) :
select_bitand PROC ;; MSVC
test cl, dl ;; ZF = !(a & b)
cmovne r9d, r8d
mov eax, r9d ;; could have done the mov to eax in parallel with the test, off the critical path, but close enough.
ret 0
ICC18 gaspille deux movzx
les instructions qui étendent à zéro le bool
s à int
mais fait ensuite le même code que MSVC
select_bitand: ## ICC18
movzx edi, dil #16.49
movzx esi, sil #16.49
test edi, esi #17.15
cmovne ecx, edx #17.15
mov eax, ecx #17.15
ret #17.15