Was ist schneller: if (bool) oder if (int)?

94

Welcher Wert ist besser zu verwenden? Boolean true oder Integer 1?

Das obige Thema hat mich dazu gebracht, einige Experimente mit boolund intin ifgutem Zustand durchzuführen . Aus Neugier schrieb ich dieses Programm:

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 generiert asm-Code für jede Funktion wie folgt:

  • ASM-Code für 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
    
  • ASM-Code für 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
    

Überraschenderweise g(bool)generiert mehr asmAnweisungen! Bedeutet das, dass if(bool)das etwas langsamer ist als if(int)? Früher dachte ich, dass booles speziell für die Verwendung in bedingten Anweisungen wie entwickelt wurde if, also hatte ich erwartet g(bool), weniger asm-Anweisungen zu generieren, um dadurch g(bool)effizienter und schneller zu werden.

BEARBEITEN:

Ich verwende derzeit kein Optimierungsflag. Aber selbst wenn es nicht vorhanden ist, warum es mehr Asm erzeugt, g(bool)ist eine Frage, auf die ich nach einer vernünftigen Antwort suche. Ich sollte Ihnen auch sagen, dass das -O2Optimierungsflag genau das gleiche asm generiert. Das ist aber nicht die Frage. Die Frage ist, was ich gestellt habe.


Nawaz
quelle
32
Es ist auch ein unfairer Test, es sei denn, Sie vergleichen sie mit angemessenen aktivierten Optimierungen.
Daniel Pryden
9
@ Daniel: Ich verwende mit keinem von beiden Optimierungsflags. Aber selbst wenn es nicht vorhanden ist, warum es mehr Asm erzeugt, g(bool)ist eine Frage, auf die ich nach einer vernünftigen Antwort suche.
Nawaz
8
Warum sollten Sie sich die Mühe machen, den ASM zu lesen, aber nicht nur das Programm auszuführen und das Ergebnis zu steuern ? Die Anzahl der Instruktionen sagt nicht viel über die Leistung aus. Sie müssen nicht nur die Befehlslängen berücksichtigen, sondern auch die Abhängigkeiten und die Arten von Befehlen (einige davon werden mithilfe des langsameren Mikrocodepfads dekodiert, welche Ausführungseinheiten sie benötigen, wie hoch die Latenz und der Durchsatz des Befehls sind, ist es a Zweig? Ein Erinnerungszugang?
Jalf
2
@user unbekannt und @Malvolio: Das ist offensichtlich; Ich mache das alles nicht für Produktionscode. Wie ich bereits am Anfang meines Beitrags erwähnt habe: "Also habe ich aus Neugier dieses Programm geschrieben" . Also ja, es ist eine rein hypothetische .
Nawaz
3
Das ist eine berechtigte Frage. Sie sind entweder gleichwertig oder einer ist schneller. Der ASM wurde wahrscheinlich veröffentlicht, um hilfreich zu sein oder laut zu denken. Verwenden Sie ihn also nicht, um der Frage auszuweichen und "nur lesbaren Code schreiben" zu sagen, sondern beantworten Sie einfach die Frage oder die STFU, wenn Sie es nicht wissen oder nicht habe nichts Nützliches zu sagen;) Mein Beitrag ist, dass die Frage beantwortbar ist und "nur lesbaren Code schreiben" nichts anderes als ein Ausweichen aus der Frage ist.
Triynko

Antworten:

99

Für mich ergibt das Sinn. Ihr Compiler definiert a anscheinend boolals 8-Bit-Wert, und Ihr System-ABI verlangt, dass kleine (<32-Bit) Ganzzahlargumente auf 32-Bit "hochgestuft" werden, wenn sie auf den Aufrufstapel übertragen werden. Um a zu vergleichen bool, generiert der Compiler Code, um das niedrigstwertige Byte des 32-Bit-Arguments, das g empfängt, zu isolieren, und vergleicht es mit cmpb. Im ersten Beispiel verwendet das intArgument die vollen 32 Bits, die auf den Stapel geschoben wurden, sodass es einfach mit dem Ganzen verglichen wird cmpl.

Sherm Pendley
quelle
4
Genau. Dies hilft zu verdeutlichen, dass Sie bei der Auswahl eines Variablentyps zwei potenziell konkurrierende Zwecke auswählen: Speicherplatz im Vergleich zur Rechenleistung.
Triynko
3
Gilt dies auch für 64-Bit-Prozesse, die __int64schneller sind als int? Oder behandelt die CPU eine 32-Bit-Ganzzahl mit 32-Bit-Befehlssätzen separat?
Crend King
1
@CrendKing Vielleicht lohnt es sich, eine andere Frage zu stellen?
Anzeigename
80

Das Kompilieren mit -03ergibt für mich Folgendes:

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

.. so stellt es im Wesentlichen den gleichen Code, mit Ausnahme cmplvs cmpb. Dies bedeutet, dass der Unterschied, falls vorhanden, keine Rolle spielt. Nach nicht optimiertem Code zu urteilen ist nicht fair.


Bearbeiten , um meinen Punkt zu verdeutlichen. Nicht optimierter Code dient dem einfachen Debuggen, nicht der Geschwindigkeit. Das Vergleichen der Geschwindigkeit von nicht optimiertem Code ist sinnlos.

Alexander Gessler
quelle
8
So sehr ich Ihrer Schlussfolgerung zustimme, denke ich, dass Sie den interessanten Teil überspringen. Warum wird es cmplfür das eine und cmpbfür das andere verwendet?
Jalf
22
@jalf: Weil a boolein einzelnes Byte und an intvier ist. Ich glaube nicht, dass es etwas Besonderes gibt.
CB Bailey
7
Ich denke, andere Antworten haben den Gründen mehr Aufmerksamkeit geschenkt: Dies liegt daran, dass die betreffende Plattform boolals 8-Bit-Typ behandelt wird.
Alexander Gessler
9
@ Nathan: Nein. C ++ hat keine Bitdatentypen. Der kleinste Typ ist charper Definition ein Byte und die kleinste adressierbare Einheit. boolDie Größe ist implementierungsdefiniert und kann 1, 4 oder 8 oder was auch immer sein. Compiler neigen jedoch dazu, es zu einem zu machen.
GManNickG
6
@ Nathan: Nun, das ist auch in Java schwierig. Java sagt, dass die Daten, die ein Boolescher Wert darstellt, der Wert eines Bits sind, aber wie dieses Bit gespeichert wird, ist noch implementiert. Pragmatische Computer adressieren einfach keine Bits.
GManNickG
26

Wenn ich dies mit einer Reihe vernünftiger Optionen kompiliere (speziell -O3), bekomme ich Folgendes:

Für 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

Für 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

Sie verwenden immer noch unterschiedliche Anweisungen für den Vergleich ( cmpbfür Boolesche vs. cmplfür Int), aber ansonsten sind die Körper identisch. Ein kurzer Blick auf die Intel-Handbücher zeigt mir: ... nicht viel von irgendetwas. Es gibt keine cmpboder cmplin den Intel-Handbüchern. Sie sind alle cmpund ich kann die Zeittabellen im Moment nicht finden. Ich vermute jedoch, dass es keinen Taktunterschied zwischen dem Vergleich eines sofortigen Bytes und dem Vergleich eines langen sofortigen Bytes gibt, sodass der Code für alle praktischen Zwecke identisch ist.


bearbeitet, um basierend auf Ihrer Hinzufügung Folgendes hinzuzufügen

Der Grund, warum sich der Code im nicht optimierten Fall unterscheidet, ist, dass er nicht optimiert ist. (Ja, es ist zirkulär, ich weiß.) Wenn der Compiler den AST durchläuft und Code direkt generiert, "weiß" er nichts außer dem, was sich unmittelbar am AST befindet. An diesem Punkt fehlen alle erforderlichen Kontextinformationen zu wissen, dass es an diesem bestimmten Punkt den deklarierten Typ boolals behandeln kann int. Ein Boolescher Wert wird offensichtlich standardmäßig als Byte behandelt. Wenn Sie Bytes in der Intel-Welt bearbeiten, müssen Sie beispielsweise die Zeichenerweiterung ausführen, um sie auf bestimmte Breiten zu bringen, damit sie auf den Stapel gelegt werden können usw. (Sie können kein Byte verschieben .)

Wenn der Optimierer den AST betrachtet und seine Magie ausübt, betrachtet er den umgebenden Kontext und "weiß", wann er Code durch etwas Effizienteres ersetzen kann, ohne die Semantik zu ändern. Es "weiß" also, dass es eine Ganzzahl im Parameter verwenden kann und dadurch die unnötigen Konvertierungen und Erweiterungen verliert.

NUR MEINE RICHTIGE MEINUNG
quelle
1
Haha, ich mag es, wie der Compiler einfach 99 oder 99 + 58 = 157 = -99 (Überlauf von signierten 8 Bits) zurückgegeben hat ... interessant.
Verlangsamte
@ Daniel: Auch das hat mir gefallen. Zuerst sagte ich "wo ist -99" und sofort wurde mir klar, dass es etwas sehr Versautes tut.
Nawaz
7
lund bsind Suffixe, die nur in der AT & T-Syntax verwendet werden. Sie beziehen sich nur auf Versionen der cmpVerwendung von 4-Byte-Operanden (lang) bzw. 1-Byte-Operanden (Byte). Wo es eine Mehrdeutigkeit in Intel - Syntax ist in herkömmlicher Weise wird der Speicheroperand getaggt mit BYTE PTR, WORD PTRoder DWORD PTRstattdessen ein Suffix auf dem Opcode des Setzens.
CB Bailey
Timing-Tabellen: agner.org/optimize Beide Operandengrößen von cmphaben die gleiche Leistung, und es gibt keine Teilregister -Strafen für das Lesen %dil . (Aber das hält Clang nicht davon ab, auf amüsante Weise einen Teilregister-Stall zu erstellen, indem die Byte-Größe andauf AL als Teil des verzweigten Fallwechsels zwischen 99 und -99 verwendet wird.)
Peter Cordes
13

Zumindest mit GCC 4.5 unter Linux und Windows sizeof(bool) == 1. Unter x86 und x86_64 können Sie nicht weniger als den Wert eines Allzweckregisters an eine Funktion übergeben (ob über den Stapel oder ein Register, abhängig von der Aufrufkonvention usw.).

Wenn der Code für bool nicht optimiert ist, wird er tatsächlich eine gewisse Länge lang, um diesen bool-Wert aus dem Argumentstapel zu extrahieren (wobei ein anderer Stapelsteckplatz zum Speichern dieses Bytes verwendet wird). Es ist komplizierter als nur eine native Variable in Registergröße zu ziehen.

Matte
quelle
Aus dem C ++ 03-Standard, §5.3.3 / 1: " sizeof(bool)und sizeof(wchar_t)sind implementierungsdefiniert. " Das Sprichwort sizeof(bool) == 1ist also nicht genau richtig, es sei denn, Sie sprechen von einer bestimmten Version eines bestimmten Compilers.
ildjarn
9

Auf Maschinenebene gibt es keinen Bool

Sehr wenige Befehlssatzarchitekturen definieren irgendeine Art von booleschem Operandentyp, obwohl es häufig Befehle gibt, die eine Aktion für Werte ungleich Null auslösen. Für die CPU ist normalerweise alles einer der Skalartypen oder eine Folge davon.

Ein gegebener Compiler und ein gegebenes ABI müssen bestimmte Größen wählen intund , boolund wenn, wie in Ihrem Fall, das sind verschiedene Größen sie leicht unterschiedlichen Code erzeugen können, und auf einigen Ebenen der Optimierung kann man etwas schneller sein.

Warum ist Bool auf vielen Systemen ein Byte?

Es ist sicherer, einen charTyp für Bool zu wählen, da jemand eine wirklich große Anzahl von ihnen erstellen könnte.

Update: von „sicherer“, ich meine: für den Compiler und Bibliothek Implementierer. Ich sage nicht, dass die Leute den Systemtyp neu implementieren müssen.

DigitalRoss
quelle
2
+1 Stellen Sie sich den Overhead auf x86 vor, wenn booler durch Bits dargestellt wird. Daher ist Byte in vielen Implementierungen ein guter Kompromiss für Geschwindigkeits- / Datenkompaktheit.
Hardmath
1
@ Billy: Ich denke, er hat nicht "Verwenden charstatt bool" gesagt, sondern einfach " charTyp" verwendet, um "1 Byte" zu bedeuten, wenn er sich auf die Größe bezieht, die der Compiler für boolObjekte auswählt .
Dennis Zickefoose
Oh, klar, ich wollte nicht, dass jedes Programm wählen sollte, ich habe nur eine Begründung dafür vorgebracht, warum der System-Bool-Typ 1 Byte ist.
DigitalRoss
@ Tennis: Ah, das macht Sinn.
Billy ONeal
7

Ja, die Diskussion macht Spaß. Aber testen Sie es einfach:

Testcode:

#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;
}

Kompiliert auf einem 64-Bit Ubuntu 10.10-Laptop mit: g ++ -O3 -o / tmp / test_i /tmp/test_i.cpp

Ganzzahliger Vergleich:

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

Boolescher Test / Druck unkommentiert (und Ganzzahl kommentiert):

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

Sie sind mit 1 Zuweisung und 2 Vergleichen pro Schleife über 30 Millionen Schleifen gleich. Finden Sie etwas anderes zu optimieren. Verwenden Sie strcmp beispielsweise nicht unnötig. ;)

dannysauer
quelle
0

Gehen Sie Ihre Frage auf zwei verschiedene Arten an:

Wenn Sie speziell über C ++ oder eine Programmiersprache sprechen, die Assembler-Code erzeugt, sind wir an den Code gebunden, den der Compiler in ASM generiert. Wir sind auch an die Darstellung von wahr und falsch in c ++ gebunden. Eine Ganzzahl muss in 32 Bit gespeichert werden, und ich könnte einfach ein Byte verwenden, um den booleschen Ausdruck zu speichern. Asm-Snippets für bedingte Anweisungen:

Für die ganze Zahl:

  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

Für den Bool:

  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

Deshalb ist der Geschwindigkeitsvergleich so kompilierungsabhängig. Im obigen Fall wäre der Bool etwas schnell, da cmpdies eine Subtraktion zum Setzen der Flags implizieren würde. Es widerspricht auch dem, was Ihr Compiler generiert hat.

Ein anderer Ansatz, der viel einfacher ist, besteht darin, die Logik des Ausdrucks selbst zu betrachten und sich keine Gedanken darüber zu machen, wie der Compiler Ihren Code übersetzt. Ich denke, dies ist eine viel gesündere Denkweise. Letztendlich glaube ich immer noch, dass der vom Compiler generierte Code tatsächlich versucht, eine wahrheitsgemäße Lösung zu finden. Was ich damit meine ist, dass der Compiler es möglicherweise so macht, dass der generierte Code mit booleschen Ausdrücken auf Maschinenebene schneller ausgeführt wird, wenn Sie die Testfälle in der if-Anweisung erhöhen und auf einer Seite boolesch und auf einer anderen ganzzahlig bleiben.

Ich denke, dies ist eine konzeptionelle Frage, also werde ich eine konzeptionelle Antwort geben. Diese Diskussion erinnert mich an Diskussionen, die ich häufig darüber habe, ob sich die Codeeffizienz in weniger Codezeilen in der Assembly niederschlägt oder nicht. Es scheint, dass dieses Konzept allgemein als wahr anerkannt wird. In Anbetracht der Tatsache, dass es nicht möglich ist, zu verfolgen, wie schnell die ALU jede Anweisung verarbeitet, besteht die zweite Option darin, sich auf Sprünge und Vergleiche in der Assembly zu konzentrieren. In diesem Fall wird die Unterscheidung zwischen booleschen Anweisungen oder Ganzzahlen in dem von Ihnen präsentierten Code ziemlich repräsentativ. Das Ergebnis eines Ausdrucks in C ++ gibt einen Wert zurück, der dann dargestellt wird. In der Montage dagegen Die Sprünge und Vergleiche basieren auf numerischen Werten, unabhängig davon, welche Art von Ausdruck bei Ihrer C ++ if-Anweisung ausgewertet wurde. Bei diesen Fragen ist es wichtig, sich daran zu erinnern, dass rein logische Aussagen wie diese einen enormen Rechenaufwand verursachen, obwohl ein einzelnes Bit dasselbe kann.

Artie
quelle