Quel est le plus rapide: if (bool) ou if (int)?

94

Quelle valeur est préférable d'utiliser? Boolean true ou Integer 1?

Le sujet ci-dessus m'a fait faire des expériences avec boolet inten ifcondition. Alors juste par curiosité, j'ai écrit ce programme:

int f(int i) 
{
    if ( i ) return 99;   //if(int)
    else  return -99;
}
int g(bool b)
{
    if ( b ) return 99;   //if(bool)
    else  return -99;
}
int main(){}

g++ intbool.cpp -S génère du code asm pour chaque fonction comme suit:

  • code asm pour f(int)

    __Z1fi:
       LFB0:
             pushl  %ebp
       LCFI0:
              movl  %esp, %ebp
       LCFI1:
              cmpl  $0, 8(%ebp)
              je    L2
              movl  $99, %eax
              jmp   L3
       L2:
              movl  $-99, %eax
       L3:
              leave
       LCFI2:
              ret
  • code asm pour g(bool)

    __Z1gb:
       LFB1:
              pushl %ebp
       LCFI3:
              movl  %esp, %ebp
       LCFI4:
              subl  $4, %esp
       LCFI5:
              movl  8(%ebp), %eax
              movb  %al, -4(%ebp)
              cmpb  $0, -4(%ebp)
              je    L5
              movl  $99, %eax
              jmp   L6
       L5:
              movl  $-99, %eax
       L6:
              leave
       LCFI6:
              ret

Étonnamment, g(bool)génère plus d' asminstructions! Cela signifie-t-il que if(bool)c'est un peu plus lent que if(int)? J'avais l'habitude de penser qu'il boolétait spécialement conçu pour être utilisé dans des instructions conditionnelles telles que if, donc je m'attendais g(bool)à générer moins d'instructions asm, ce qui rendrait g(bool)plus efficace et plus rapide.

ÉDITER:

Je n'utilise aucun indicateur d'optimisation pour le moment. Mais même son absence, pourquoi génère-t-elle plus d'ASM pour g(bool)est une question pour laquelle je cherche une réponse raisonnable. Je dois également vous dire que l' -O2indicateur d'optimisation génère exactement le même asm. Mais ce n'est pas la question. La question est ce que j'ai demandé.


Nawaz
la source
32
C'est aussi un test injuste à moins que vous ne les compariez avec des optimisations raisonnables activées.
Daniel Pryden
9
@Daniel: Je n'utilise aucun indicateur d'optimisation avec l'un ou l'autre. Mais même en son absence, pourquoi génère-t-il plus d'ASM pour g(bool)une question pour laquelle je cherche une réponse raisonnable.
Nawaz
8
Pourquoi voudriez-vous vous donner la peine de lire l'asm, mais pas seulement d'exécuter le programme et de chronométrer le résultat ? Le nombre d'instructions ne dit pas grand-chose sur les performances. Vous devez prendre en compte non seulement la longueur des instructions, mais aussi les dépendances et les types d'instructions (certaines d'entre elles sont-elles décodées à l'aide du chemin de microcode plus lent, quelles unités d'exécution ont-elles besoin, quelle est la latence et le débit de l'instruction, est-ce un branch? A memmory access?
jalf
2
@user unknown, et @Malvolio: C'est évidemment; Je ne fais pas tout cela pour le code de production. Comme je l'ai déjà mentionné au début de mon article, "Donc, par curiosité, j'ai écrit ce programme" . Alors oui, c'est purement hypothétique .
Nawaz
3
C'est une question légitime. Ils sont soit équivalents, soit l'un est plus rapide. L'ASM a probablement été posté dans le but d'être utile ou de réfléchir à haute voix, donc plutôt que de l'utiliser comme un moyen d'esquiver la question et de dire "écrivez simplement un code lisible", répondez simplement à la question ou STFU si vous ne savez pas ou Je n'ai rien d'utile à dire;) Ma contribution est que la question est répondable, et "écrire simplement du code lisible" n'est rien d'autre qu'une esquive de la question.
Triynko

Réponses:

99

Cela a du sens pour moi. Votre compilateur définit apparemment a boolcomme une valeur 8 bits, et votre système ABI lui demande de "promouvoir" les petits arguments entiers (<32 bits) en 32 bits lors de leur insertion dans la pile d'appels. Donc, pour comparer a bool, le compilateur génère du code pour isoler l'octet le moins significatif de l'argument 32 bits que g reçoit et le compare avec cmpb. Dans le premier exemple, l' intargument utilise les 32 bits complets qui ont été poussés sur la pile, il se compare donc simplement à l'ensemble avec cmpl.

Sherm Pendley
la source
4
Je suis d'accord. Cela aide à éclairer que lors du choix d'un type de variable, vous le choisissez pour deux objectifs potentiellement concurrents, l'espace de stockage par rapport aux performances de calcul.
Triynko
3
Cela s'applique-t-il également aux processus 64 bits, c'est-à- __int64dire plus rapide que int? Ou le CPU traite séparément un entier 32 bits avec des jeux d'instructions 32 bits?
Crend King
1
@CrendKing vaut peut-être la peine de poser une autre question?
Afficher le nom
81

Compiler avec -03me donne ce qui suit:

F:

    pushl   %ebp
    movl    %esp, %ebp
    cmpl    $1, 8(%ebp)
    popl    %ebp
    sbbl    %eax, %eax
    andb    $58, %al
    addl    $99, %eax
    ret

g:

    pushl   %ebp
    movl    %esp, %ebp
    cmpb    $1, 8(%ebp)
    popl    %ebp
    sbbl    %eax, %eax
    andb    $58, %al
    addl    $99, %eax
    ret

.. il compile essentiellement au même code, à l' exception cmplvs cmpb. Cela signifie que la différence, s'il y en a, n'a pas d'importance. Juger par un code non optimisé n'est pas juste.


Modifier pour clarifier mon point. Le code non optimisé sert au débogage simple, pas à la vitesse. Comparer la vitesse d'un code non optimisé est insensé.

Alexander Gessler
la source
8
Autant que je suis d'accord avec votre conclusion, je pense que vous sautez la partie intéressante. Pourquoi l' utilise- t -il cmplpour l'un et cmpbpour l'autre?
jalf
22
@jalf: Parce que a boolest un octet et an intest quatre. Je ne pense pas qu'il y ait rien de plus spécial que ça.
CB Bailey
7
Je pense que d'autres réponses ont prêté plus d'attention aux raisons: c'est parce que la plate-forme en question traite boolcomme un type 8 bits.
Alexander Gessler
9
@Nathan: Non. C ++ n'a pas de types de données bit. Le plus petit type est char, qui est un octet par définition, et est la plus petite unité adressable. boolLa taille de est définie par l'implémentation et peut être 1, 4 ou 8, ou autre. Cependant, les compilateurs ont tendance à en faire un.
GManNickG
6
@Nathan: Eh bien, c'est aussi compliqué en Java. Java dit que les données représentées par un booléen sont la valeur d'un bit, mais la manière dont ce bit est stocké est toujours définie par l'implémentation. Les ordinateurs pragmatiques n'adressent tout simplement pas les bits.
GManNickG
26

Lorsque je compile cela avec un ensemble d'options raisonnable (en particulier -O3), voici ce que j'obtiens:

Pour f():

        .type   _Z1fi, @function
_Z1fi:
.LFB0:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        cmpl    $1, %edi
        sbbl    %eax, %eax
        andb    $58, %al
        addl    $99, %eax
        ret
        .cfi_endproc

Pour g():

        .type   _Z1gb, @function
_Z1gb:
.LFB1:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        cmpb    $1, %dil
        sbbl    %eax, %eax
        andb    $58, %al
        addl    $99, %eax
        ret
        .cfi_endproc

Ils utilisent toujours des instructions différentes pour la comparaison ( cmpbpour booléen vs cmplpour int), mais sinon les corps sont identiques. Un rapide coup d'œil aux manuels d'Intel me dit: ... pas grand-chose. Il n'y a rien de tel que cmpbou cmpldans les manuels Intel. Ils sont tous cmpet je ne trouve pas les horaires pour le moment. Je suppose, cependant, qu'il n'y a pas de différence d'horloge entre comparer un octet immédiat et comparer un long immédiat, donc à toutes fins pratiques, le code est identique.


modifié pour ajouter ce qui suit en fonction de votre ajout

La raison pour laquelle le code est différent dans le cas non optimisé est qu'il n'est pas optimisé. (Oui, c'est circulaire, je sais.) Lorsque le compilateur parcourt l'AST et génère directement du code, il ne «sait» rien sauf ce qui se trouve au point immédiat de l'AST dans lequel il se trouve. À ce stade, il lui manque toutes les informations contextuelles nécessaires pour savoir qu'à ce stade précis, il peut traiter le type déclaré boolcomme un int. Un booléen est évidemment traité par défaut comme un octet et lors de la manipulation d'octets dans le monde Intel, vous devez faire des choses comme sign-extend pour l'amener à certaines largeurs pour le mettre sur la pile, etc. (Vous ne pouvez pas pousser un octet .)

Quand l'optimiseur voit l'AST et fait sa magie, cependant, il regarde le contexte environnant et "sait" quand il peut remplacer le code par quelque chose de plus efficace sans changer la sémantique. Ainsi, il "sait" qu'il peut utiliser un entier dans le paramètre et ainsi perdre les conversions et élargissements inutiles.

JUSTE MON AVIS correct
la source
1
Haha, j'aime la façon dont le compilateur a simplement renvoyé 99, ou 99 + 58 = 157 = -99 (débordement de 8 bits signés) ... intéressant.
deceleratedcaviar
@Daniel: Même moi, j'ai aimé ça. Au début, j'ai dit "où est -99" et j'ai tout de suite réalisé que ça faisait quelque chose de très pervers.
Nawaz
7
let bsont des suffixes utilisés dans la syntaxe AT&T uniquement. Ils se réfèrent simplement à des versions cmputilisant respectivement des opérandes de 4 octets (longs) et 1 octets (octets). Là où il y a une ambiguïté dans la syntaxe d'Intel, l'opérande de mémoire est classiquement étiqueté avec BYTE PTR, WORD PTRou DWORD PTRau lieu de mettre un suffixe sur l'opcode.
CB Bailey
Tables de synchronisation: agner.org/optimize Les deux tailles d'opérande de cmpont les mêmes performances, et il n'y a pas de pénalités de registre partiel pour la lecture %dil . (Mais cela n'empêche pas de créer de manière amusante un blocage de registre partiel en utilisant la taille d'octet andsur AL dans le cadre d'un retournement de cas sans branche entre 99 et -99.)
Peter Cordes
13

Avec GCC 4.5 sur Linux et Windows au moins, sizeof(bool) == 1. Sur x86 et x86_64, vous ne pouvez pas passer moins que la valeur d'un registre à usage général à une fonction (que ce soit via la pile ou un registre en fonction de la convention d'appel etc ...).

Ainsi, le code de bool, lorsqu'il n'est pas optimisé, va en fait à une certaine longueur pour extraire cette valeur booléenne de la pile d'arguments (en utilisant un autre emplacement de pile pour enregistrer cet octet). C'est plus compliqué que de simplement extraire une variable native de la taille d'un registre.

Tapis
la source
De la norme C ++ 03, §5.3.3 / 1: " sizeof(bool)et sizeof(wchar_t)sont définis par l'implémentation. " Ce sizeof(bool) == 1n'est pas strictement correct à moins que vous ne parliez d'une version spécifique d'un compilateur spécifique.
ildjarn
9

Au niveau de la machine, il n'y a pas de booléen

Très peu d'architectures de jeux d'instructions définissent une sorte de type d'opérande booléen, bien qu'il y ait souvent des instructions qui déclenchent une action sur des valeurs non nulles. Pour le processeur, généralement, tout est l'un des types scalaires ou une chaîne d'entre eux.

Un compilateur donné et un ABI donné devront choisir des tailles spécifiques pour intet boolet quand, comme dans votre cas, ce sont des tailles différentes, ils peuvent générer du code légèrement différent, et à certains niveaux d'optimisation, un peut être légèrement plus rapide.

Pourquoi booléen est-il un octet sur de nombreux systèmes?

Il est plus sûr de choisir un chartype pour booléen car quelqu'un pourrait en créer un très grand nombre.

Mise à jour: par «plus sûr», je veux dire: pour les implémenteurs de compilateurs et de bibliothèques. Je ne dis pas que les gens doivent réimplémenter le type de système.

DigitalRoss
la source
2
+1 Imaginez le surcoût sur x86 s'il boolétait représenté par des bits; donc byte sera un bon compromis pour la vitesse / la compacité des données dans de nombreuses implémentations.
hardmath
1
@Billy: Je pense qu'il ne disait pas "utiliser à la charplace de bool" mais à la place simplement utilisé " chartype" pour signifier "1 octet" en se référant à la taille que le compilateur choisit pour les boolobjets.
Dennis Zickefoose
Oh, bien sûr, je ne voulais pas dire que chaque programme devrait choisir, je voulais juste avancer une justification pour expliquer pourquoi le type de booléen système est de 1 octet.
DigitalRoss
@Dennis: Ah, c'est logique.
Billy ONeal
7

Ouais, la discussion est amusante. Mais testez-le simplement:

Code de test:

#include <stdio.h>
#include <string.h>

int testi(int);
int testb(bool);
int main (int argc, char* argv[]){
  bool valb;
  int  vali;
  int loops;
  if( argc < 2 ){
    return 2;
  }
  valb = (0 != (strcmp(argv[1], "0")));
  vali = strcmp(argv[1], "0");
  printf("Arg1: %s\n", argv[1]);
  printf("BArg1: %i\n", valb ? 1 : 0);
  printf("IArg1: %i\n", vali);
  for(loops=30000000; loops>0; loops--){
    //printf("%i: %i\n", loops, testb(valb=!valb));
    printf("%i: %i\n", loops, testi(vali=!vali));
  }
  return valb;
}

int testi(int val){
  if( val ){
    return 1;
  }
  return 0;
}
int testb(bool val){
  if( val ){
    return 1;
  }
  return 0;
}

Compilé sur un ordinateur portable Ubuntu 10.10 64 bits avec: g ++ -O3 -o / tmp / test_i /tmp/test_i.cpp

Comparaison basée sur des nombres entiers:

sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.203s
user    0m8.170s
sys 0m0.010s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.056s
user    0m8.020s
sys 0m0.000s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.116s
user    0m8.100s
sys 0m0.000s

Test booléen / impression non commentée (et entier commenté):

sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.254s
user    0m8.240s
sys 0m0.000s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.028s
user    0m8.000s
sys 0m0.010s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m7.981s
user    0m7.900s
sys 0m0.050s

Ils sont les mêmes avec 1 affectation et 2 comparaisons, chaque boucle sur 30 millions de boucles. Trouvez autre chose à optimiser. Par exemple, n'utilisez pas strcmp inutilement. ;)

Dannysauer
la source
0

Aborder votre question de deux manières différentes:

Si vous parlez spécifiquement de C ++ ou de tout autre langage de programmation qui produira du code d'assemblage pour cette question, nous sommes liés au code que le compilateur générera dans ASM. Nous sommes également liés à la représentation de vrai et faux en c ++. Un entier devra être stocké en 32 bits, et je pourrais simplement utiliser un octet pour stocker l'expression booléenne. Extraits ASM pour les instructions conditionnelles:

Pour l'entier:

  mov eax,dword ptr[esp]    ;Store integer
  cmp eax,0                 ;Compare to 0
  je  false                 ;If int is 0, its false
  ;Do what has to be done when true
false:
  ;Do what has to be done when false

Pour le booléen:

  mov  al,1     ;Anything that is not 0 is true
  test al,1     ;See if first bit is fliped
  jz   false    ;Not fliped, so it's false
  ;Do what has to be done when true
false:
  ;Do what has to be done when false

C'est pourquoi la comparaison de vitesse est si dépendante de la compilation. Dans le cas ci-dessus, la valeur booléenne serait légèrement rapide car cmpimpliquerait une soustraction pour définir les indicateurs. Cela contredit également ce que votre compilateur a généré.

Une autre approche, beaucoup plus simple, consiste à examiner la logique de l'expression seule et à ne pas vous soucier de la façon dont le compilateur traduira votre code, et je pense que c'est une façon de penser beaucoup plus saine. Je crois toujours, en fin de compte, que le code généré par le compilateur essaie en fait de donner une résolution véridique. Ce que je veux dire, c'est que, peut-être que si vous augmentez les cas de test dans l'instruction if et que vous vous en tenez avec booléen d'un côté et entier dans un autre, le compilateur fera en sorte que le code généré s'exécute plus rapidement avec des expressions booléennes au niveau de la machine.

Je considère que c'est une question conceptuelle, je vais donc donner une réponse conceptuelle. Cette discussion me rappelle les discussions que j'ai couramment sur la question de savoir si l'efficacité du code se traduit ou non par moins de lignes de code dans l'assemblage. Il semble que ce concept soit généralement accepté comme étant vrai. Étant donné que le suivi de la vitesse à laquelle l'ALU traitera chaque instruction n'est pas viable, la deuxième option serait de se concentrer sur les sauts et les comparaisons dans l'assemblage. Lorsque c'est le cas, la distinction entre les instructions booléennes ou les entiers dans le code que vous avez présenté devient plutôt représentative. Le résultat d'une expression en C ++ renverra une valeur qui recevra ensuite une représentation. En montage, en revanche, les sauts et les comparaisons seront basés sur des valeurs numériques quel que soit le type d'expression qui a été évalué dans votre instruction if C ++. Il est important sur ces questions de se rappeler que des déclarations purement logiques comme celles-ci se retrouvent avec une surcharge de calcul énorme, même si un seul bit serait capable de la même chose.

Artie
la source