47 votes

Les valeurs booléennes comme 8 bits dans les compilateurs. Les opérations sur ces valeurs sont-elles inefficaces ?

Je suis en train de lire "Agner Fog". Optimisation des logiciels en C++ "(spécifique aux processeurs x86 pour Intel, AMD et VIA) et il est dit à la page 34

Les variables booléennes sont stockées sous forme d'entiers de 8 bits avec la valeur 0 pour faux et 1 pour vrai. Les variables booléennes sont surdéterminées dans le sens où tous les opérateurs ayant des variables booléennes en entrée booléennes en entrée vérifient si les entrées ont une autre valeur que 0 ou 1, mais les opérateurs qui ont des booléens en sortie ne peuvent produire aucune autre valeur. booléens en sortie ne peuvent produire aucune autre valeur que 0 ou 1. Cela rend les opérations avec des variables booléennes en entrée moins efficaces que nécessaire.

Est-ce encore vrai aujourd'hui et sur quels compilateurs ? Pouvez-vous donner un exemple ? L'auteur déclare

Les opérations booléennes peuvent être rendues beaucoup plus efficaces si elles si l'on sait avec certitude que les opérandes n'ont pas d'autres valeurs que 0 ou 1. 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.

Cela signifie-t-il que si je prends un pointeur de fonction bool(*)() par exemple et l'appeler, alors les opérations sur celui-ci produisent un code inefficace ? Ou est-ce le cas lorsque j'accède à un booléen en déréférençant un pointeur ou en lisant une référence et que j'effectue ensuite des opérations sur celui-ci ?

69voto

Peter Cordes Points 1375

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

7voto

geza Points 13730

Je pense que ce n'est pas le cas.

Tout d'abord, ce raisonnement est totalement inacceptable :

La raison pour laquelle le compilateur ne fait pas une telle supposition est que les variables pourraient avoir d'autres valeurs si elles sont non initialisées ou proviennent de sources inconnues.

Vérifions un peu de code (compilé avec clang 6, mais GCC 7 et MSVC 2017 produisent un code similaire).

Booléen ou :

bool fn(bool a, bool b) {
    return a||b;
}

0000000000000000 <fn(bool, bool)>:
   0:   40 08 f7                or     dil,sil
   3:   40 88 f8                mov    al,dil
   6:   c3                      ret    

Comme on peut le voir, pas de contrôle 0/1 ici, simple or .

Convertit un bool en int :

int fn(bool a) {
    return a;
}

0000000000000000 <fn(bool)>:
   0:   40 0f b6 c7             movzx  eax,dil
   4:   c3                      ret    

Encore une fois, pas de contrôle, un simple mouvement.

Convertit un caractère en un bool :

bool fn(char a) {
    return a;
}

0000000000000000 <fn(char)>:
   0:   40 84 ff                test   dil,dil
   3:   0f 95 c0                setne  al
   6:   c3                      ret    

Ici, char est vérifié s'il est égal à 0, ou non, et la valeur de bool est fixée à 0 ou 1 en conséquence.

Je pense donc que l'on peut dire que le compilateur utilise bool d'une manière telle qu'il contient toujours un 0/1. Il ne vérifie jamais sa validité.

A propos de l'efficacité : Je pense que bool est optimal. Le seul cas que je peux imaginer, où cette approche n'est pas optimale est la conversion char->bool. Cette opération pourrait être un simple mov, si la valeur bool n'était pas limitée à 0/1. Pour toutes les autres opérations, l'approche actuelle est aussi bonne, voire meilleure.


EDIT : Peter Cordes a mentionné l'ABI. Voici le texte pertinent de l'ABI de System V pour AMD64 (le texte pour i386 est similaire) :

Booléens, lorsqu'ils sont stockés dans un objet de mémoire, sont stockés sous forme d'un seul octet dont la valeur est toujours 0 (faux) ou 1 (vrai). . Lorsque dans des registres de nombres entiers (sauf en cas de passage en tant qu'arguments), les 8 octets du registre sont significatifs. octets du registre sont significatifs ; toute valeur non nulle est considérée comme vraie

Ainsi, pour les plateformes qui suivent l'ABI de SysV, nous pouvons être sûrs qu'une bool a une valeur de 0/1.

J'ai recherché le document ABI pour MSVC, mais malheureusement je n'ai rien trouvé concernant bool .

0voto

Tony D Points 43962

J'ai compilé ce qui suit avec clang++ -O3 -S

bool andbool(bool a, bool b)
{
    return a && b;
}

bool andint(int a, int b)
{
    return a && b;
}

Le site .s contient :

andbool(bool, bool):                           # @andbool(bool, bool)
    andb    %sil, %dil
    movl    %edi, %eax
    retq

andint(int, int):                            # @andint(int, int)
    testl   %edi, %edi
    setne   %cl
    testl   %esi, %esi
    setne   %al
    andb    %cl, %al
    retq

Il est clair que c'est la version bool qui en fait moins.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X