it-swarm.com.de

Ist <schneller als <=?

Ich lese ein Buch, in dem der Autor sagt, dass if( a < 901 ) schneller ist als if( a <= 900 ).

Nicht genau wie in diesem einfachen Beispiel, aber es gibt geringfügige Leistungsänderungen bei komplexem Code für Schleifen. Ich nehme an, das hat etwas mit dem generierten Maschinencode zu tun, falls es überhaupt wahr ist.

Nein, auf den meisten Architekturen wird es nicht schneller sein. Sie haben nicht angegeben, aber auf x86 werden alle integralen Vergleiche normalerweise in zwei Maschinenanweisungen implementiert:

  • Eine test oder cmp Anweisung, die EFLAGS setzt
  • Und eine Jcc (Sprung) -Anweisung , abhängig vom Vergleichstyp (und Codelayout):
    • jne - Springe, wenn nicht gleich -> ZF = 0
    • jz - Springe wenn Null (gleich) -> ZF = 1
    • jg - Springe wenn größer -> ZF = 0 and SF = OF
    • (usw...)

Beispiel (der Kürze halber bearbeitet) Kompiliert mit $ gcc -m32 -S -masm=intel test.c

    if (a < b) {
        // Do something 1
    }

Kompiliert zu:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

Und

    if (a <= b) {
        // Do something 2
    }

Kompiliert zu:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

Der einzige Unterschied zwischen den beiden ist also eine Anweisung jg gegenüber einer Anweisung jge. Die beiden werden die gleiche Zeit in Anspruch nehmen.


Ich möchte auf den Kommentar eingehen, dass nichts darauf hindeutet, dass die verschiedenen Sprungbefehle dieselbe Zeit benötigen. Diese Frage ist etwas schwierig zu beantworten, aber hier ist, was ich sagen kann: In der Intel Instruction Set Reference sind alle unter einer gemeinsamen Anweisung zusammengefasst: Jcc (Jump if condition is getroffen). Dieselbe Gruppierung wird im Optimization Reference Manual in Anhang C vorgenommen. Latenz und Durchsatz.

Latenz - Die Anzahl der Taktzyklen, die der Ausführungskern benötigt, um die Ausführung aller μops abzuschließen, die einen Befehl bilden.

Durchsatz - Die Anzahl der Taktzyklen, die erforderlich sind, um zu warten, bis die Ausgabeports die gleiche Anweisung erneut annehmen können. Bei vielen Befehlen kann der Durchsatz eines Befehls erheblich geringer sein als seine Latenz

Die Werte für Jcc sind:

      Latency   Throughput
Jcc     N/A        0.5

mit folgender Fußnote zu Jcc:

7) Die Auswahl von Anweisungen für bedingte Sprünge sollte auf der Empfehlung in Abschnitt 3.4.1, „Optimierung der Verzweigungsvorhersage“ basieren, um die Vorhersagbarkeit von Verzweigungen zu verbessern. Wenn Verzweigungen erfolgreich vorhergesagt werden, ist die Latenz von jcc effektiv Null.

Daher behandelt nichts in den Intel-Dokumenten jemals eine Jcc -Anweisung anders als die anderen.

Wenn man über die tatsächliche Schaltung nachdenkt, die zur Implementierung der Anweisungen verwendet wird, kann man davon ausgehen, dass es einfache UND/ODER-Gatter für die verschiedenen Bits in EFLAGS gibt, um festzustellen, ob die Bedingungen erfüllt sind. Es gibt dann keinen Grund, warum ein Befehl, der zwei Bits testet, mehr oder weniger Zeit als ein Befehl, der nur eines testet, benötigen sollte.


Bearbeiten: Gleitkomma

Dies gilt auch für x87-Gleitkommazahlen: (Ziemlich derselbe Code wie oben, jedoch mit double anstelle von int.)

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret
1645

Historisch (wir sprechen von den 1980er und frühen 1990er Jahren) gab es einige Architekturen, in denen dies wahr war. Das Hauptproblem ist, dass der Ganzzahlvergleich inhärent über Ganzzahlsubtraktionen implementiert wird. Daraus ergeben sich folgende Fälle.

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

Nun, wenn A < B die Subtraktion ein hohes Bit ausleihen muss, damit die Subtraktion korrekt ist, so wie Sie sie tragen und ausleihen, wenn Sie von Hand addieren und subtrahieren. Dieses "geliehene" Bit wurde normalerweise als Übertragsbit bezeichnet und konnte durch einen Verzweigungsbefehl getestet werden. Ein zweites Bit, das Nullbit genannt wird, würde gesetzt, wenn die Subtraktion identisch Null wäre, was Gleichheit impliziert.

Es gab normalerweise mindestens zwei bedingte Verzweigungsbefehle, einen zum Verzweigen auf dem Übertragsbit und einen auf dem Nullbit.

Lassen Sie uns nun die vorherige Tabelle erweitern, um die Übertrags- und Null-Bit-Ergebnisse einzuschließen.

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

Das Implementieren einer Verzweigung für A < B kann also in einem Befehl erfolgen, da das Übertragsbit eindeutig ist nur in diesem Fall, d. H.

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

Wenn wir jedoch einen Vergleich durchführen möchten, der kleiner oder gleich ist, müssen wir das Null-Flag zusätzlich prüfen, um den Fall der Gleichheit zu erfassen.

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

Also, auf einigen Maschinen, unter Verwendung eines "less than" -Vergleichs könnte save eine Maschinenanweisung. Dies war im Zeitalter der Sub-Megahertz-Prozessorgeschwindigkeit und des 1: 1-CPU-zu-Speicher-Geschwindigkeitsverhältnisses von Bedeutung, ist aber heute nahezu irrelevant.

584
Lucas

Angenommen, es handelt sich um interne Integer-Typen, dann gibt es keinen Weg, wie einer schneller sein könnte als der andere. Sie sind offensichtlich semantisch identisch. Beide fordern den Compiler auf, genau dasselbe zu tun. Nur ein schrecklich kaputter Compiler würde einen minderwertigen Code für einen dieser Compiler generieren.

Wenn es eine Plattform gab, auf der < für einfache Integer-Typen schneller war als <=, sollte der Compiler immer<= in < für Konstanten konvertieren. Jeder Compiler, der nicht nur ein schlechter Compiler wäre (für diese Plattform).

88
David Schwartz

Ich sehe, dass keiner schneller ist. Der Compiler generiert in jeder Bedingung den gleichen Maschinencode mit einem anderen Wert.

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

Mein Beispiel if stammt von GCC auf der x86_64-Plattform unter Linux.

Compiler-Autoren sind ziemlich kluge Leute, und sie denken über diese und viele andere Dinge nach, die die meisten von uns für selbstverständlich halten.

Mir ist aufgefallen, dass in beiden Fällen derselbe Maschinencode generiert wird, wenn es sich nicht um eine Konstante handelt.

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3
66
Adrian Cornish

Für Gleitkommacode kann der Vergleich <= sogar auf modernen Architekturen (um eine Anweisung) langsamer sein. Hier ist die erste Funktion:

int compare_strict(double a, double b) { return a < b; }

Auf einem PowerPC führt dies zuerst einen Gleitkomma-Vergleich durch (der cr, das Bedingungsregister, aktualisiert), verschiebt dann das Bedingungsregister zu einem GPR, verschiebt das Bit "Verglichen weniger als" an die richtige Stelle und kehrt dann zurück. Es dauert vier Anweisungen.

Betrachten Sie stattdessen diese Funktion:

int compare_loose(double a, double b) { return a <= b; }

Dies erfordert die gleiche Arbeit wie compare_strict oben, aber jetzt gibt es zwei interessante Punkte: "war kleiner als" und "war gleich". Dies erfordert einen zusätzlichen Befehl (cror - Bedingungsregister bitweise ODER), um diese beiden Bits zu einem zu kombinieren. Also benötigt compare_loose fünf Befehle, während compare_strict vier benötigt.

Sie könnten denken, dass der Compiler die zweite Funktion folgendermaßen optimieren könnte:

int compare_loose(double a, double b) { return ! (a > b); }

Dies wird jedoch falsch mit NaNs umgehen. NaN1 <= NaN2 und NaN1 > NaN2 müssen beide mit false bewertet werden.

50
ridiculous_fish

Vielleicht hat der Autor dieses unbenannten Buches gelesen, dass a > 0 schneller läuft als a >= 1 und glaubt, dass dies universell zutrifft.

Dies liegt jedoch daran, dass ein 0 beteiligt ist (da CMP je nach Architektur beispielsweise durch OR ersetzt werden kann) und nicht an dem <.

34
glglgl

Zumindest, wenn dies wahr wäre, könnte ein Compiler a <= b to! (A> b) trivial optimieren, und selbst wenn der Vergleich selbst langsamer wäre, würden Sie mit Ausnahme des naivsten Compilers keinen Unterschied bemerken .

32
Eliot Ball

Sie haben die gleiche Geschwindigkeit. Vielleicht stimmt das, was er/sie sagte, in einer speziellen Architektur, aber in der x86-Familie weiß ich zumindest, dass sie gleich sind. Denn dazu führt die CPU eine Subtraktion (a - b) durch und prüft dann die Flags des Flag-Registers. Zwei Bits dieses Registers heißen ZF (Zero Flag) und SF (Sign Flag) und werden in einem Zyklus ausgeführt, da dies mit einer Maskenoperation ausgeführt wird.

15
Masoud

Dies hängt in hohem Maße von der zugrunde liegenden Architektur ab, zu der das C kompiliert wurde. Einige Prozessoren und Architekturen verfügen möglicherweise über explizite Anweisungen für gleich oder kleiner als und gleich, die in einer unterschiedlichen Anzahl von Zyklen ausgeführt werden.

Das wäre allerdings ziemlich ungewöhnlich, da der Compiler es umgehen könnte, was es irrelevant macht.

14
Telgin

TL; DR Antwort

Für die meisten Kombinationen aus Architektur, Compiler und Sprache ist dies nicht schneller.

Volle Antwort

Andere Antworten haben sich auf die x86 - Architektur konzentriert, und ich kenne die ARM - Architektur (die Ihr Beispielassembler zu sein scheint) nicht gut genug, um den Code speziell zu kommentieren generiert, aber dies ist ein Beispiel für eine Mikrooptimierung welche sehr architekturspezifisch ist und genauso wahrscheinlich ist eine Anti-Optimierung sein, wie es eine Optimierung sein soll .

Als solches würde ich vorschlagen, dass diese Art von Mikrooptimierung ein Beispiel für Frachtkult Programmierung ist und nicht die beste Softwareentwicklungspraxis.

Es gibt wahrscheinlich einige Architekturen, bei denen dies eine Optimierung ist, aber ich kenne mindestens eine Architektur, bei der das Gegenteil der Fall sein kann. Die ehrwürdige Transputer Architektur hatte nur Maschinencodeanweisungen für gleich und größer oder gleich , Daher mussten alle Vergleiche aus diesen Primitiven erstellt werden.

Selbst dann konnte der Compiler in fast allen Fällen die Auswertungsanweisungen so anordnen, dass in der Praxis kein Vergleich einen Vorteil gegenüber anderen hatte. Im schlimmsten Fall muss möglicherweise eine umgekehrte Anweisung (REV) hinzugefügt werden, um die beiden obersten Elemente im Operandenstapel zu vertauschen. Dies war ein Einzelbyte-Befehl, dessen Ausführung einen einzigen Zyklus in Anspruch nahm und daher den geringstmöglichen Overhead aufwies.

Ob es sich bei einer solchen Mikrooptimierung um eine Optimierung oder eine Antioptimierung handelt, hängt von der verwendeten Architektur ab. Daher ist es in der Regel eine schlechte Idee, sich an architekturspezifische Mikrooptimierungen zu gewöhnen. Andernfalls könnten Sie eine instinktiv verwenden, wenn dies nicht angebracht ist, und es sieht so aus, als befürworte das Buch, das Sie lesen, genau dies.

11
Mark Booth

Sie sollten den Unterschied nicht bemerken können, selbst wenn es einen gibt. Außerdem müssen Sie in der Praxis einen zusätzlichen a + 1 oder a - 1 ausführen, um die Bedingung aufrechtzuerhalten, es sei denn, Sie verwenden einige magische Konstanten, was auf jeden Fall eine sehr schlechte Praxis ist.

6
shinkou

Man könnte sagen, dass die Zeile in den meisten Skriptsprachen korrekt ist, da das zusätzliche Zeichen zu einer etwas langsameren Codeverarbeitung führt. Wie in der Antwort oben bereits erwähnt, sollte es in C++ keine Auswirkungen haben, und bei der Arbeit mit einer Skriptsprache geht es wahrscheinlich nicht so sehr um Optimierung.

4
Ecksters

Als ich diese Antwort schrieb, habe ich nur die Titelfrage zu <vs. <= im Allgemeinen betrachtet, nicht das spezifische Beispiel einer Konstante a < 901 vs. a <= 900. Viele Compiler verkleinern die Größe von Konstanten immer, indem sie zwischen < und <= konvertieren, z. weil x86-Direktoperanden eine kürzere 1-Byte-Codierung für -128..127 haben.

Für ARM und insbesondere für AArch64 hängt es von der Möglichkeit ab, ein schmales Feld in eine beliebige Position in einem Word zu drehen, um als Sofortcode codieren zu können. So wäre cmp w0, #0x00f000 codierbar, während cmp w0, #0x00effff möglicherweise nicht codierbar ist. Daher gilt für AArch64 nicht immer die Verkleinerungsregel für den Vergleich mit einer Kompilierungszeitkonstante.


<vs. <= im Allgemeinen, auch für Bedingungen mit Laufzeitvariablen

In der Assemblersprache der meisten Computer hat ein Vergleich für <= dieselben Kosten wie ein Vergleich für <. Dies gilt unabhängig davon, ob Sie darauf verzweigen, es booleanisieren, um eine Ganzzahl von 0/1 zu erstellen, oder es als Prädikat für eine verzweigungslose Auswahloperation (wie x86-CMOV) verwenden. Die anderen Antworten haben nur diesen Teil der Frage angesprochen.

Bei dieser Frage geht es jedoch um die C++ - Operatoren, die - Eingabe für den Optimierer. Normalerweise sind beide gleich effizient. Der Ratschlag aus dem Buch klingt völlig falsch, weil Compiler den Vergleich, den sie in asm implementieren, immer transformieren können. Es gibt jedoch mindestens eine Ausnahme, bei der die Verwendung von <= versehentlich zu Ergebnissen führen kann, die der Compiler nicht optimieren kann.

Als Schleifenbedingung gibt es Fälle, in denen <= qualitativ von < abweicht, wenn der Compiler keine Schleife mehr beweisen kann ist nicht unendlich. Dies kann einen großen Unterschied machen und die automatische Vektorisierung deaktivieren.

Ein vorzeichenloser Überlauf wird im Gegensatz zu einem vorzeichenbehafteten Überlauf (UB) als Base-2-Wrap-Around definiert. Vorzeichenschleifenzähler sind in der Regel vor diesem Verhalten geschützt, wenn Compiler, die auf der Grundlage von UB mit Vorzeichen optimieren, nicht ausgeführt werden: ++i <= size wird letztendlich immer falsch. ( Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte )

void foo(unsigned size) {
    unsigned upper_bound = size - 1;  // or any calculation that could produce UINT_MAX
    for(unsigned i=0 ; i <= upper_bound ; i++)
        ...

Compiler können nur so optimieren, dass das (definierte und gesetzlich beobachtbare) Verhalten der C++ - Quelle für all mögliche Eingabewerte erhalten bleibt, mit Ausnahme derjenigen, die zu führen undefiniertes Verhalten.

(Ein einfacher i <= size würde das Problem ebenfalls verursachen, aber ich dachte, die Berechnung einer Obergrenze wäre ein realistischeres Beispiel für die versehentliche Einführung der Möglichkeit einer Endlosschleife für eine Eingabe, die Sie nicht interessieren, die der Compiler jedoch berücksichtigen muss .)

In diesem Fall führt size=0 zu upper_bound=UINT_MAX, und i <= UINT_MAX ist immer wahr. Diese Schleife ist also unendlich für size=0, und der Compiler muss dies berücksichtigen, obwohl Sie als Programmierer wahrscheinlich nie vorhaben, size = 0 zu übergeben. Wenn der Compiler diese Funktion in einen Aufrufer einbinden kann, wo er beweisen kann, dass size = 0 unmöglich ist, kann er wie bei i < size optimieren.

Asm wie if(!size) skip the loop;do{...}while(--size); ist eine normalerweise effiziente Möglichkeit, eine for( i<size ) -Schleife zu optimieren, wenn der tatsächliche Wert von i in der Schleife nicht benötigt wird ( Warum werden Schleifen immer im Stil "do ... while" kompiliert (Tail Jump)? ).

Aber das {} while kann nicht unendlich sein: Wenn es mit size==0 eingegeben wird, erhalten wir 2 ^ n Iterationen. ( Durchlaufen aller vorzeichenlosen ganzen Zahlen in einer for-Schleife C ermöglicht das Ausdrücken einer Schleife über alle vorzeichenlosen ganzen Zahlen, einschließlich Null, aber es ist nicht einfach, ohne ein Übertragsflag wie in asm.)

Da der Umlauf des Schleifenzählers eine Möglichkeit ist, geben moderne Compiler häufig einfach auf und optimieren nicht annähernd so aggressiv.

Beispiel: Summe von ganzen Zahlen von 1 bis n

sing unsigned i <= n besiegt die Spracherkennung von Clang, die sum(1 .. n) Schleifen mit einer geschlossenen Form optimiert ​​basierend auf der n * (n+1) / 2 Formel von Gauss.

unsigned sum_1_to_n_finite(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i < n+1 ; ++i)
        total += i;
    return total;
}

x86-64 asm von clang7.0 und gcc8.2 im Godbolt-Compiler-Explorer

 # clang7.0 -O3 closed-form
    cmp     edi, -1       # n passed in EDI: x86-64 System V calling convention
    je      .LBB1_1       # if (n == UINT_MAX) return 0;  // C++ loop runs 0 times
          # else fall through into the closed-form calc
    mov     ecx, edi         # zero-extend n into RCX
    lea     eax, [rdi - 1]   # n-1
    imul    rax, rcx         # n * (n-1)             # 64-bit
    shr     rax              # n * (n-1) / 2
    add     eax, edi         # n + (stuff / 2) = n * (n+1) / 2   # truncated to 32-bit
    ret          # computed without possible overflow of the product before right shifting
.LBB1_1:
    xor     eax, eax
    ret

Aber für die naive Version bekommen wir nur eine blöde Schleife von clang.

unsigned sum_1_to_n_naive(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i<=n ; ++i)
        total += i;
    return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
    xor     ecx, ecx           # i = 0
    xor     eax, eax           # retval = 0
.LBB0_1:                       # do {
    add     eax, ecx             # retval += i
    add     ecx, 1               # ++1
    cmp     ecx, edi
    jbe     .LBB0_1            # } while( i<n );
    ret

GCC verwendet in keiner Weise eine geschlossene Form, sodass die Wahl der Schleifenbedingung nicht wirklich schadet; Es wird automatisch mit der SIMD-Ganzzahladdition vektorisiert, wobei 4 i Werte in den Elementen eines XMM-Registers parallel ausgeführt werden.

# "naive" inner loop
.L3:
    add     eax, 1       # do {
    paddd   xmm0, xmm1    # vect_total_4.6, vect_vec_iv_.5
    paddd   xmm1, xmm2    # vect_vec_iv_.5, tmp114
    cmp     edx, eax      # bnd.1, ivtmp.14     # bound and induction-variable tmp, I think.
    ja      .L3 #,       # }while( n > i )

 "finite" inner loop
  # before the loop:
  # xmm0 = 0 = totals
  # xmm1 = {0,1,2,3} = i
  # xmm2 = set1_epi32(4)
 .L13:                # do {
    add     eax, 1       # i++
    paddd   xmm0, xmm1    # total[0..3] += i[0..3]
    paddd   xmm1, xmm2    # i[0..3] += 4
    cmp     eax, edx
    jne     .L13      # }while( i != upper_limit );

     then horizontal sum xmm0
     and peeled cleanup for the last n%3 iterations, or something.

Es hat auch eine einfache skalare Schleife, die meiner Meinung nach für sehr kleine n und/oder für den Endlosschleifenfall verwendet wird.

Übrigens verschwenden beide Schleifen einen Befehl (und einen UOP bei CPUs der Sandybridge-Familie) für den Schleifen-Overhead. sub eax,1/jnz anstelle von add eax,1/cmp/jcc wäre effizienter. 1 uop statt 2 (nach Makrofusion von sub/jcc oder cmp/jcc). Der Code nach beiden Schleifen schreibt EAX bedingungslos, sodass nicht der Endwert des Schleifenzählers verwendet wird.

3
Peter Cordes

Nur wenn die Leute, die die Computer erstellt haben, schlecht mit der Booleschen Logik umgehen können. Was sie nicht sein sollten.

Jeder Vergleich (>=<=><) kann mit der gleichen Geschwindigkeit durchgeführt werden.

Was jeder Vergleich ist, ist nur eine Subtraktion (der Unterschied) und zu sehen, ob es positiv/negativ ist.
(Wenn msb eingestellt ist, ist die Zahl negativ)

Wie überprüfe ich a >= b? Sub a-b >= 0 Prüfen Sie, ob a-b positiv ist.
Wie überprüfe ich a <= b? Sub 0 <= b-a Prüfen Sie, ob b-a positiv ist.
Wie überprüfe ich a < b? Sub a-b < 0 Überprüfen Sie, ob a-b negativ ist.
Wie überprüfe ich a > b? Sub 0 > b-a Überprüfen Sie, ob b-a negativ ist.

Einfach ausgedrückt, der Computer kann dies für die gegebene Operation einfach unter der Haube tun:

a >= b == msb(a-b)==0
a <= b == msb(b-a)==0
a > b == msb(b-a)==1
a < b == msb(a-b)==1

und natürlich müsste der Computer auch nicht den ==0 oder ==1 ausführen.
für den ==0 könnte es nur den msb aus der Schaltung invertieren.

Auf jeden Fall hätten sie es mit Sicherheit nicht geschafft, a >= b als a>b || a==b zu berechnen. Lol

0
Puddle