Vermeiden Sie Postfix Increment Operator

25

Ich habe gelesen, dass ich den Postfix-Inkrement-Operator aus Leistungsgründen vermeiden sollte (in bestimmten Fällen).

Beeinträchtigt dies jedoch nicht die Lesbarkeit des Codes? Meiner Meinung nach:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

Sieht besser aus als:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

Aber das ist wahrscheinlich nur aus Gewohnheit. Zugegeben, ich habe nicht viele Anwendungen gesehen ++i.

Ist die Leistung in diesem Fall so schlecht, dass die Lesbarkeit beeinträchtigt wird? Oder bin ich einfach blind und ++ilesbarer als i++?

Mateen Ulhaq
quelle
1
Ich habe verwendet, i++bevor ich wusste, dass es die Leistung beeinträchtigen könnte ++i, also habe ich gewechselt. Zuerst sah letzteres etwas seltsam aus, aber nach einer Weile habe ich mich daran gewöhnt und jetzt fühlt es sich so natürlich an wie i++.
Gablin
15
++iund i++mache verschiedene Dinge in bestimmten Kontexten, nimm nicht an, dass sie gleich sind.
Orbling
2
Geht es um C oder C ++? Das sind zwei sehr unterschiedliche Sprachen! :-) In C ++ ist die idiomatische for-Schleife for (type i = 0; i != 42; ++i). Kann nicht nur operator++überladen werden, sondern auch operator!=und operator<. Präfix-Inkrement ist nicht teurer als Postfix, ungleich ist nicht teurer als kleiner als. Welche sollen wir verwenden?
Bo Persson
7
Sollte es nicht ++ C heißen?
Armand
21
@Stephen: C ++ bedeutet, dass Sie C nehmen, es hinzufügen und dann das alte verwenden .
Supercat

Antworten:

58

Die Fakten:

  1. i ++ und ++ i sind gleich gut lesbar. Sie mögen keine, weil Sie nicht daran gewöhnt sind, aber es gibt im Grunde nichts, was Sie falsch interpretieren können, so dass es keine Arbeit mehr zum Lesen oder Schreiben gibt.

  2. In einigen Fällen ist der Postfix-Operator weniger effizient.

  3. In 99,99% der Fälle spielt es jedoch keine Rolle, weil (a) es sowieso auf einen einfachen oder primitiven Typ wirkt und es nur ein Problem ist, wenn es ein großes Objekt kopiert. (B) es wird nicht in einer Aufführung sein Kritischer Teil von Code (c) Sie wissen nicht, ob der Compiler ihn optimieren wird oder nicht.

  4. Daher empfehle ich die Verwendung des Präfix, es sei denn, Sie benötigen speziell Postfix. Dies ist eine gute Angewohnheit, nur weil (a) es eine gute Angewohnheit ist, mit anderen Dingen präzise umzugehen, und (b) wenn Sie einmal einen blauen Mond erreicht haben, werden Sie beabsichtigen, Postfix zu verwenden und verstehe es falsch: Wenn du immer schreibst, was du meinst, ist das weniger wahrscheinlich. Es gibt immer einen Kompromiss zwischen Leistung und Optimierung.

Sie sollten Ihren gesunden Menschenverstand verwenden und nicht mikrooptimieren, bis Sie es brauchen, aber auch nicht ineffizient sein. In der Regel bedeutet dies: Schließen Sie zunächst jede Codekonstruktion aus, die selbst in nicht zeitkritischem Code inakzeptabel ineffizient ist (normalerweise ein grundlegender konzeptioneller Fehler, z. B. das Übergeben von 500 MB-Objekten als Wert ohne Grund). und zweitens wählen Sie von jeder anderen Art, den Code zu schreiben, die klarste.

Allerdings glaube ich, dass die Antwort einfach ist: Ich glaube, dass das Schreiben eines Präfixes, es sei denn, Sie benötigen speziell Postfix, (a) sehr unwahrscheinlich klarer und (b) sehr unwahrscheinlich effizienter ist, also sollten Sie das immer standardmäßig schreiben, aber Mach dir keine Sorgen, wenn du es vergisst.

Vor sechs Monaten dachte ich genauso wie Sie, dass i ++ natürlicher sei, aber es ist rein das, was Sie gewohnt sind.

BEARBEITUNG 1: Scott Meyers in "Effektiveres C ++", dem ich im Allgemeinen vertraue, sagt, dass Sie generell die Verwendung des Postfix-Operators für benutzerdefinierte Typen vermeiden sollten (da die einzige vernünftige Implementierung der Postfix-Inkrementierungsfunktion darin besteht, a zu erstellen Kopieren Sie das Objekt, rufen Sie die Funktion zum Inkrementieren des Präfix auf, um das Inkrementieren durchzuführen, und geben Sie die Kopie zurück. Kopiervorgänge können jedoch teuer sein.

Wir wissen also nicht, ob es allgemeine Regeln gibt, um (a) ob dies heute zutrifft, (b) ob dies auch (weniger) für intrinsische Typen gilt, (c) ob Sie "++" verwenden sollten etwas mehr als eine leichte Iteratorklasse überhaupt. Aber aus all den Gründen, die ich oben beschrieben habe, spielt es keine Rolle, was ich vorher gesagt habe.

EDIT 2: Dies bezieht sich auf die allgemeine Praxis. Wenn Sie denken, dass es in einem bestimmten Fall wichtig ist, sollten Sie es profilieren und sehen. Die Profilerstellung ist einfach und kostengünstig und funktioniert. Aus den ersten Prinzipien abzuleiten, was optimiert werden muss, ist schwierig und teuer und funktioniert nicht.

Jack V.
quelle
Ihre Buchung ist direkt am Geld. In Ausdrücken, in denen der Operator infix + und post-increment ++ überladen wurden, z. B. aClassInst = someOtherClassInst + yetAnotherClassInst ++, generiert der Parser Code zur Ausführung der additiven Operation, bevor er den Code zur Ausführung der post-increment-Operation generiert Erstellen Sie eine temporäre Kopie. Der Performance-Killer ist hier kein Post-Inkrement. Es ist die Verwendung eines überladenen Infix-Operators. Infix-Operatoren erzeugen neue Instanzen.
Bit Twiddler
2
Ich vermute sehr, dass der Grund, warum die Leute i++eher daran gewöhnt sind als ++idaran, dass in dieser Frage / Antwort auf den Namen einer bestimmten populären Programmiersprache verwiesen wird ...
Shadow
61

Code immer zuerst für den Programmierer und dann für den Computer.

Wenn es einen Leistungsunterschied gibt, nachdem der Compiler Ihren Code genauestens überprüft hat, UND Sie ihn messen können UND es darauf ankommt, können Sie ihn ändern.

Martin Beckett
quelle
7
SUPERB Aussage !!!
Dave
8
@Martin: Genau aus diesem Grund würde ich ein Präfixinkrement verwenden. Postfix-Semantik impliziert, dass der alte Wert beibehalten wird. Wenn er nicht benötigt wird, ist die Verwendung ungenau.
Matthieu M.
1
Für einen Schleifenindex wäre das klarer - aber wenn Sie über ein Array iterieren würden, indem Sie einen Zeiger inkrementieren und ein Präfix verwenden, bedeute dies, dass Sie vor dem Start an einer unzulässigen Adresse beginnen, was ungeachtet eines Leistungszuwachses schlecht wäre
Martin Beckett,
5
@Matthew: Es ist einfach nicht wahr, dass nach dem Inkrementieren eine Kopie des alten Werts beibehalten werden muss. Man kann nicht sicher sein, wie ein Compiler mit Zwischenwerten umgeht, bis man seine Ausgabe betrachtet. Wenn Sie sich die Zeit nehmen, meine mit Anmerkungen versehene Liste der von GCC generierten Assemblersprachen anzuzeigen, werden Sie feststellen, dass GCC für beide Schleifen denselben Maschinencode generiert. Dieser Unsinn, das Pre-Inkrement dem Post-Inkrement vorzuziehen, weil es effizienter ist, ist kaum mehr als eine Vermutung.
Bit-Twiddler
2
@ Mathhieu: Der Code, den ich gepostet habe, wurde mit deaktivierter Optimierung generiert. Die C ++ - Spezifikation besagt nicht, dass ein Compiler eine temporäre Instanz eines Werts erzeugen muss, wenn eine Nachinkrementierung verwendet wird. Es wird lediglich der Vorrang der Operatoren vor und nach dem Inkrement angegeben.
Bit-Twiddler
13

GCC erzeugt für beide Schleifen den gleichen Maschinencode.

C-Code

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

Assembly Code (mit meinen Kommentaren)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols
Bit-Twiddler
quelle
Wie wäre es mit aktivierter Optimierung?
serv-inc
2
@user: Wahrscheinlich keine Änderung, aber erwarten Sie tatsächlich, dass Bit-Twiddler bald zurückkommt?
Deduplizierer
2
Achtung: Während es in C keine benutzerdefinierten Typen mit überladenen Operatoren gibt, gibt es in C ++ solche, und das Verallgemeinern von Basistypen auf benutzerdefinierte Typen ist einfach ungültig .
Deduplizierer
@ Deduplicator: Vielen Dank, auch für den Hinweis, dass diese Antwort nicht auf benutzerdefinierte Typen verallgemeinert wird. Ich hatte seine Benutzerseite nicht angesehen, bevor ich gefragt habe.
serv-inc
12

Mach dir keine Sorgen über die Leistung, sagen wir 97% der Zeit. Vorzeitige Optimierung ist die Wurzel allen Übels.

- Donald Knuth

Nachdem dies nicht möglich ist, treffen wir unsere Wahl mit Bedacht :

  • ++i: Präfixinkrement , inkrementiert den aktuellen Wert und liefert das Ergebnis
  • i++: Inkrementiere Postfix , kopiere den Wert, inkrementiere den aktuellen Wert, ergebe die Kopie

Sofern keine Kopie des alten Werts erforderlich ist, ist die Verwendung des Postfix-Inkrements eine einfache Möglichkeit, um die Dinge zu erledigen.

Ungenauigkeit entsteht durch Faulheit. Verwenden Sie immer das Konstrukt, das Ihre Absicht am direktesten zum Ausdruck bringt. Es besteht weniger Wahrscheinlichkeit, dass der zukünftige Betreuer Ihre ursprüngliche Absicht missversteht.

Auch wenn es hier (wirklich) geringfügig ist, gibt es Zeiten, in denen ich durch das Lesen von Code wirklich verwirrt war: Ich habe mich wirklich gefragt, ob die Absicht und der tatsächliche Ausdruck übereinstimmten, und natürlich haben sie (oder ich) nach ein paar Monaten erinnerte mich auch nicht ...

Es spielt also keine Rolle, ob es für Sie richtig aussieht oder nicht. Umarme KISS . In ein paar Monaten werden Sie Ihre alten Praktiken meiden.

Matthieu M.
quelle
4

In C ++ können Sie einen erheblichen Leistungsunterschied bewirken, wenn es sich um eine Überlastung von Operatoren handelt, insbesondere wenn Sie Vorlagencode schreiben und nicht wissen, welche Iteratoren möglicherweise übergeben werden. Das ist langsam und kann vom Compiler nicht optimiert werden.

Dies ist jedoch nicht der Fall in C, wo Sie wissen, dass es sich nur um einen trivialen Typ handelt und der Leistungsunterschied geringfügig ist und der Compiler leicht optimieren kann.

Also ein Tipp: Sie programmieren in C oder C ++ und Fragen beziehen sich auf das eine oder andere, nicht auf beide.

DeadMG
quelle
2

Die Leistung einer der beiden Operationen hängt stark von der zugrunde liegenden Architektur ab. Man muss einen gespeicherten Wert inkrementieren, was bedeutet, dass der von Neumann-Engpass in beiden Fällen der begrenzende Faktor ist.

Im Fall von ++ i müssen wir

Fetch i from memory 
Increment i
Store i back to memory
Use i

Im Fall von i ++ müssen wir

Fetch i from memory
Use i
Increment i
Store i back to memory

Die Operatoren ++ und - führen ihren Ursprung auf den PDP-11-Befehlssatz zurück. Der PDP-11 könnte ein automatisches Nachinkrementieren eines Registers durchführen. Es könnte auch ein automatisches Vordekrementieren einer in einem Register enthaltenen effektiven Adresse durchführen. In beiden Fällen konnte der Compiler diese Operationen auf Maschinenebene nur dann nutzen, wenn die betreffende Variable eine "Register" -Variable war.

Bit-Twiddler
quelle
2

Wenn Sie wissen möchten, ob etwas langsam ist, testen Sie es. Nehmen Sie eine BigInteger oder ein Äquivalent, stecken Sie sie in eine ähnliche for-Schleife und verwenden Sie beide Redewendungen. Stellen Sie sicher, dass das Innere der Schleife nicht optimiert wird, und messen Sie beide.

Nachdem ich den Artikel gelesen habe, finde ich ihn aus drei Gründen nicht sehr überzeugend. Erstens sollte der Compiler in der Lage sein, die Erstellung eines Objekts zu optimieren, das niemals verwendet wird. Zweitens ist das i++Konzept für numerische for-Schleifen idiomatisch , sodass sich die Fälle, von denen ich sehe, dass sie tatsächlich betroffen sind, auf beschränken. Drittens liefern sie ein rein theoretisches Argument ohne Zahlen, die es stützen könnten.

Vor allem aufgrund von Grund 1 gehe ich davon aus, dass sie direkt nebeneinander liegen, wenn Sie das Timing tatsächlich durchführen.

Jprete
quelle
-1

Erstens hat es keinen Einfluss auf die Lesbarkeit IMO. Es ist nicht das, woran Sie gewöhnt sind, aber es würde nur eine kurze Zeit dauern, bis Sie sich daran gewöhnt haben.

Zweitens werden Sie wahrscheinlich keinen großen Unterschied feststellen, es sei denn, Sie verwenden eine Menge Postfix-Operatoren in Ihrem Code. Das Hauptargument dafür, sie nach Möglichkeit nicht zu verwenden, ist, dass eine Kopie des Werts der ursprünglichen Variable bis zum Ende der Argumente aufbewahrt werden muss, in denen die ursprüngliche Variable noch verwendet werden kann. Das sind je nach Architektur entweder 32 oder 64 Bit. Das entspricht 4 oder 8 Bytes oder 0,00390625 oder 0,0078125 MB. Es ist sehr wahrscheinlich, dass Sie bei den heutigen Computerressourcen und der Geschwindigkeit nicht einmal einen Unterschied bemerken, wenn Sie nicht eine Tonne von ihnen verwenden, die für einen sehr langen Zeitraum gespeichert werden müssen, um von Postfix zu Präfix zu wechseln.

BEARBEITEN: Vergessen Sie diesen verbleibenden Teil, da meine Schlussfolgerung als falsch erwiesen wurde (abgesehen davon, dass ++ i und i ++ nicht immer dasselbe tun ... das ist immer noch wahr).

Vorhin wurde auch darauf hingewiesen, dass sie in einigen Fällen nicht dasselbe tun. Seien Sie vorsichtig beim Umschalten, wenn Sie dies wünschen. Ich habe es noch nie ausprobiert (ich habe immer Postfix verwendet), daher weiß ich es nicht genau, aber ich denke, der Wechsel von Postfix zu Präfix führt zu unterschiedlichen Ergebnissen: (Auch hier kann ich mich irren ... hängt vom Compiler ab / Dolmetscher auch)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}
Kenneth
quelle
4
Die Inkrementierungsoperation findet am Ende der for-Schleife statt, sodass sie genau dieselbe Ausgabe haben würden. Es ist nicht abhängig vom Compiler / Interpreter.
Sternberg
@jsternberg ... Danke Ich war mir nicht sicher, wann das Inkrement passierte, da ich nie wirklich einen Grund hatte, es jemals auszuprobieren. Es ist zu lange her, dass ich Compiler im College gemacht habe! lol
Kenneth
Falsch falsch falsch.
Ruohola
-1

Ich denke semantisch, ++imacht mehr Sinn als i++, also würde ich mich an die erste halten, außer es ist üblich, dies nicht zu tun (wie in Java, wo Sie es verwenden sollten, i++weil es weit verbreitet ist).

Oliver Weiler
quelle
-2

Es geht nicht nur um Leistung.

Manchmal möchten Sie das Kopieren gar nicht erst implementieren, weil es keinen Sinn ergibt. Und da die Verwendung des Präfixinkrements nicht davon abhängt, ist es einfach, sich an das Präfixformular zu halten.

Und verschiedene Inkremente für primitive und komplexe Typen zu verwenden, ist wirklich unlesbar.

maxim1000
quelle
-2

Wenn du es nicht wirklich brauchst, bleibe ich bei ++ i. In den meisten Fällen ist dies beabsichtigt. Es ist nicht sehr häufig, dass Sie i ++ benötigen, und Sie müssen immer zweimal überlegen, wenn Sie ein solches Konstrukt lesen. Mit ++ i ist es ganz einfach: Sie fügen 1 hinzu, verwenden es und dann ist es immer noch dasselbe.

Also, ich stimme voll und ganz @martin beckett zu: Mach es dir leichter, es ist schon schwer genug.

Peter Frings
quelle