i++ weniger effizient als ++i, wie kann man das zeigen?

Lesezeit: 9 Minuten

i weniger effizient als i wie kann man das zeigen
Schule

Ich versuche an einem Beispiel zu zeigen, dass das Präfixinkrement effizienter ist als das Postfixinkrement.

Theoretisch ist dies sinnvoll: i++ muss in der Lage sein, den nicht inkrementierten Originalwert zurückzugeben und daher zu speichern, während ++i den inkrementierten Wert zurückgeben kann, ohne den vorherigen Wert zu speichern.

Aber gibt es ein gutes Beispiel, um dies in der Praxis zu zeigen?

Ich habe folgenden Code probiert:

int array[100];

int main()
{
  for(int i = 0; i < sizeof(array)/sizeof(*array); i++)
    array[i] = 1;
}

Ich habe es mit gcc 4.4.0 wie folgt kompiliert:

gcc -Wa,-adhls -O0 myfile.cpp

Ich habe dies erneut getan, wobei das Postfix-Inkrement in ein Präfix-Inkrement geändert wurde:

for(int i = 0; i < sizeof(array)/sizeof(*array); ++i)

Das Ergebnis ist in beiden Fällen identischer Assemblercode.

Dies war etwas unerwartet. Es schien so, als ob ich durch das Ausschalten der Optimierungen (mit -O0) einen Unterschied sehen sollte, um das Konzept zu zeigen. Was vermisse ich? Gibt es ein besseres Beispiel, um dies zu zeigen?

  • Der Compiler ist schlau genug, um abzuleiten, dass ++i und i++ in Ihrem Schleifenbeispiel das gleiche Ergebnis liefern würden. Versuchen Sie, das Ergebnis tatsächlich zu verwenden, indem Sie es einer Variablen zuweisen und etwas damit berechnen, wie zum Beispiel einen Array-Index oder so. Aber ich wage zu behaupten, dass Sie vernachlässigbare Unterschiede sehen werden.

    – Lasse V. Karlsen

    12. Juli ’09 um 19:44

  • übrigens: sizeof(array)/sizeof(array[0])… sizeof(*array)=sizeof(int)

    – Artjom

    12. Juli ’09 um 19:47

  • Sie übersehen, dass der Compiler schlauer ist als Sie. In Ihrem Beispielcode haben i++ und ++i auf jeden Fall denselben Effekt. Das ist nicht einmal eine Compiler-Optimierung, sondern nur normale Codegenerierung. Wenn Sie einen anderen Assemblercode mit deaktivierter Optimierung wünschen, erstellen Sie Code, in dem i++ und ++i Teil eines größeren Ausdrucks sind. (Sie werden feststellen, dass es egal ist, wenn Sie die Optimierungen wieder aktivieren). Sie sollten eher post vs preincrement in C++ testen, wo es einen kleinen Unterschied machen kann (z. B. bei Iteratoren).

    – nein

    12. Juli ’09 um 20:52

  • Bei der Ganzzahl werden Sie wahrscheinlich keinen Unterschied sehen, der Compiler kann einige intelligente Optimierungen vornehmen. Möglicherweise können Sie einen Unterschied bei benutzerdefinierten Typen feststellen, bei denen das Vor- und Nach-Inkrement korrekt definiert ist (dh sie verhalten sich wie die eingebauten Typen).

    – Martin York

    12. Juli ’09 um 22:08

  • @Artyom: Ja, sicher, aber wenn Sie sich jemals entscheiden würden, den Typ zu ändern, dann array speichert in seinen Elementen, werden Sie wahrscheinlich mit einem Fehler enden, wenn Sie verwenden sizeof(int) anstatt sizeof(*array) da die Wahrscheinlichkeit groß ist, dass Sie vergessen, sich auch an dieser Stelle umzuziehen. Der Code ändert sich ständig. Versuchen Sie daher immer, Änderungen so einfach wie möglich zu machen, was normalerweise bedeutet, die Anzahl der Stellen zu reduzieren, die Sie im Code ändern müssen, damit eine Änderung stattfindet, insbesondere wenn es sich um Stellen handelt das wird kompiliert, auch wenn Sie sie nicht ändern (wie in diesem Fall).

    – Hallo Auf Wiedersehen

    7. Januar ’14 um 15:15

1641859119 891 i weniger effizient als i wie kann man das zeigen
Michael Burr

Im Allgemeines In diesem Fall führt das Post-Inkrement zu einer Kopie, bei der ein Pre-Increment dies nicht tut. Natürlich wird dies in vielen Fällen wegoptimiert und in den Fällen, in denen dies nicht der Fall ist, wird der Kopiervorgang vernachlässigbar sein (zB für eingebaute Typen).

Hier ist ein kleines Beispiel, das die potenzielle Ineffizienz der Post-Inkrementierung zeigt.

#include <stdio.h>

class foo 
{

public:
    int x;

    foo() : x(0) { 
        printf( "construct foo()n"); 
    };

    foo( foo const& other) { 
        printf( "copy foo()n"); 
        x = other.x; 
    };

    foo& operator=( foo const& rhs) { 
        printf( "assign foo()n"); 
        x = rhs.x;
        return *this; 
    };

    foo& operator++() { 
        printf( "preincrement foon"); 
        ++x; 
        return *this; 
    };

    foo operator++( int) { 
        printf( "postincrement foon"); 
        foo temp( *this);
        ++x;
        return temp; 
    };

};


int main()
{
    foo bar;

    printf( "n" "preinc example: n");
    ++bar;

    printf( "n" "postinc example: n");
    bar++;
}

Die Ergebnisse eines optimierten Builds (der im Post-Increment-Fall aufgrund von RVO tatsächlich einen zweiten Kopiervorgang entfernt):

construct foo()

preinc example: 
preincrement foo

postinc example: 
postincrement foo
copy foo()

Im Allgemeinen, wenn Sie die Semantik des Post-Inkrement nicht benötigen, warum gehen Sie dann das Risiko ein, dass eine unnötige Kopie auftritt?

Natürlich ist es gut zu bedenken, dass ein benutzerdefinierter Operator++() – entweder die Pre- oder die Post-Variante – zurückgeben kann, was immer er will (oder sogar tun, was er will), und ich kann mir vorstellen, dass es einige gibt die nicht den üblichen Regeln folgen. Gelegentlich bin ich auf Implementierungen gestoßen, die “void“, wodurch der übliche semantische Unterschied verschwindet.

  • Dieses Beispiel veranschaulicht deutlich das Problem von i++ – wenn i ein komplexer Typ ist, bei dem das Kopieren teuer ist (vielleicht ein ausgeklügelter referenzzählender Iterator), dann muss das Kopieren vermieden werden.

    – Tom Leys

    12. Juli ’09 um 20:50

Bei Ganzzahlen werden Sie keinen Unterschied sehen. Sie müssen Iteratoren oder etwas verwenden, bei dem Post und Präfix wirklich etwas anderes tun. Und Sie müssen alle Optimierungen drehen an, nicht aus!

  • Können Sie diesen Optimierungskommentar bitte näher ausführen? Danke!

    – Schule

    12. Juli ’09 um 19:54

  • Bei jedem Benchmarking müssen Sie den Compiler dazu bringen, den bestmöglichen Code zu generieren. Bei geringer Optimierung können alle möglichen Faktoren, wie eine schlechte Registerausnutzung, ins Spiel kommen und die Ergebnisse verfälschen. Ich bin davon gebissen worden, als ich hier Benchmarks gepostet habe.

    anon

    12. Juli ’09 um 20:00

  • Ich glaube, er will nur sehen, dass ohne Optimierung, ++x sollte schnelleren Code generieren als x++.

    – GManNickG

    12. Juli ’09 um 20:06

  • @GMan Mein Punkt ist, dass ohne Optimierungen andere Faktoren dem Nachweis im Wege stehen können, selbst wenn dies in einer idealen Welt wahr wäre.

    anon

    12. Juli ’09 um 20:11

  • +1 kurze, vollständige Antwort. speziell für die Erwähnung der Optimierungsfalle.

    – mmx

    12. Juli ’09 um 20:28

Ich folge gerne der Regel “Sag, was du meinst”.

++i einfach inkrementiert. i++ Schritte und hat ein besonderes, nicht intuitives Bewertungsergebnis. ich benutze nur i++ wenn ich dieses Verhalten ausdrücklich will und verwende ++i in allen anderen Fällen. Wenn Sie diese Praxis befolgen, wenn Sie sehen, i++ im Code ist es offensichtlich, dass das Verhalten nach dem Inkrementieren wirklich beabsichtigt war.

  • Ich wette, wenn die Sprache ++C genannt worden wäre, würden die Leute dies standardmäßig viel häufiger tun.

    – Reunanen

    12. Juli ’09 um 20:25

  • Vielleicht wird es C++ genannt, weil es sich immer noch wie C verhält, wenn es auf eine bestimmte Weise verwendet wird.

    – Tom Leys

    12. Juli ’09 um 20:48

  • “Wenn Sie i++ im Code sehen, ist es offensichtlich, dass das Verhalten nach dem Inkrementieren wirklich beabsichtigt war.” Sie müssen einer der wenigen besonderen sein, die sich an den Unterschied erinnern 😉

    – quant_dev

    13. Juli ’09 um 6:28

1641859119 680 i weniger effizient als i wie kann man das zeigen
Nathan Fellmann

Dieser Code und seine Kommentare sollten die Unterschiede zwischen den beiden veranschaulichen.

class a {
    int index;
    some_ridiculously_big_type big;

    //etc...

};

// prefix ++a
void operator++ (a& _a) {
    ++_a.index
}

// postfix a++
void operator++ (a& _a, int b) {
    _a.index++;
}

// now the program
int main (void) {
    a my_a;

    // prefix:
    // 1. updates my_a.index
    // 2. copies my_a.index to b
    int b = (++my_a).index; 

    // postfix
    // 1. creates a copy of my_a, including the *big* member.
    // 2. updates my_a.index
    // 3. copies index out of the **copy** of my_a that was created in step 1
    int c = (my_a++).index; 
}

Sie können sehen, dass das Postfix einen zusätzlichen Schritt (Schritt 1) ​​hat, der das Erstellen eines Kopieren des Objekts. Dies hat sowohl Auswirkungen auf den Speicherverbrauch als auch auf die Laufzeit. Dass Deshalb ist Präfix effizienter als Postfix für Nicht-Grundkenntnisse Typen.

Es hängt davon ab some_ridiculously_big_type Und auch bei allem, was Sie mit dem Ergebnis der Erhöhung tun, können Sie den Unterschied mit oder ohne Optimierungen sehen.

Als Antwort auf Mihail ist dies eine etwas portablere Version seines Codes:

#include <cstdio>
#include <ctime>
using namespace std;

#define SOME_BIG_CONSTANT 100000000
#define OUTER 40
int main( int argc, char * argv[] ) {

    int d = 0;
    time_t now = time(0);
    if ( argc == 1 ) {
        for ( int n = 0; n < OUTER; n++ ) {
            int i = 0;
            while(i < SOME_BIG_CONSTANT) {
                d += i++;
            }
        }
    }
    else {
        for ( int n = 0; n < OUTER; n++ ) {
            int i = 0;
            while(i < SOME_BIG_CONSTANT) {
                d += ++i;
            }
        }
    }
    int t = time(0) - now;  
    printf( "%dn", t );
    return d % 2;
}

Die äußeren Schleifen sind dazu da, es mir zu ermöglichen, am Timing herumzufummeln, um etwas Passendes auf meiner Plattform zu finden.

Ich verwende VC++ nicht mehr, also habe ich es (unter Windows) kompiliert mit:

g++ -O3 t.cpp

Ich habe es dann abwechselnd ausgeführt:

a.exe   

und

a.exe 1

Meine Timing-Ergebnisse waren in beiden Fällen ungefähr gleich. Manchmal wäre eine Version um bis zu 20 % schneller und manchmal die andere. Ich würde vermuten, dass dies auf andere Prozesse zurückzuführen ist, die auf meinem System ausgeführt werden.

  • Es scheint also ein ganz anderes Verhalten zu haben, wenn es mit verschiedenen Tools kompiliert und auf verschiedenen Plattformen ausgeführt wird.

    – Michail Churbanov

    13. Juli ’09 um 8:44

  • Bitte versuchen Sie meinen Code mit Ihrem Compiler, bevor Sie diese Annahme treffen.

    anon

    13. Juli ’09 um 8:48

  • 10-11 vs 13. Je komplizierter das Programm ist, desto weniger Unterschiede in ++ werden Sie sehen.

    – Michail Churbanov

    13. Juli ’09 um 9:29

Versuchen Sie, while zu verwenden oder etwas mit dem zurückgegebenen Wert zu tun, zB:

#define SOME_BIG_CONSTANT 1000000000

int _tmain(int argc, _TCHAR* argv[])
{
    int i = 1;
    int d = 0;

    DWORD d1 = GetTickCount();
    while(i < SOME_BIG_CONSTANT + 1)
    {
        d += i++;
    }
    DWORD t1 = GetTickCount() - d1;

    printf("%d", d);
    printf("ni++ > %d <n", t1);

    i = 0;
    d = 0;

    d1 = GetTickCount();
    while(i < SOME_BIG_CONSTANT)
    {
        d += ++i;

    }
    t1 = GetTickCount() - d1;

    printf("%d", d);
    printf("n++i > %d <n", t1);

    return 0;
}

Kompiliert mit VS 2005 mit /O2 oder /Ox, auf meinem Desktop und Laptop ausprobiert.

Auf dem Laptop stabil herumkommen, auf dem Desktop sind die Zahlen etwas anders (aber die Rate ist ungefähr gleich):

i++ > 8xx < 
++i > 6xx <

xx bedeutet, dass die Zahlen unterschiedlich sind, z. B. 813 vs. 640 – immer noch etwa 20% schneller.

Und noch ein Punkt – wenn Sie “d +=” durch “d = ” ersetzen, sehen Sie einen schönen Optimierungstrick:

i++ > 935 <
++i > 0 <

Es ist jedoch ziemlich spezifisch. Aber schließlich sehe ich keinen Grund, meine Meinung zu ändern und denke, es gibt keinen Unterschied 🙂

  • Es scheint also ein ganz anderes Verhalten zu haben, wenn es mit verschiedenen Tools kompiliert und auf verschiedenen Plattformen ausgeführt wird.

    – Michail Churbanov

    13. Juli ’09 um 8:44

  • Bitte versuchen Sie meinen Code mit Ihrem Compiler, bevor Sie diese Annahme treffen.

    anon

    13. Juli ’09 um 8:48

  • 10-11 vs 13. Je komplizierter das Programm ist, desto weniger Unterschiede in ++ werden Sie sehen.

    – Michail Churbanov

    13. Juli ’09 um 9:29

Vielleicht könnten Sie nur den theoretischen Unterschied zeigen, indem Sie beide Versionen mit x86-Montageanleitungen schreiben? Wie viele Leute schon früher darauf hingewiesen haben, trifft der Compiler immer seine eigenen Entscheidungen darüber, wie das Programm am besten kompiliert/assembliert wird.

Wenn das Beispiel für Studenten gedacht ist, die mit dem x86-Befehlssatz nicht vertraut sind, könnten Sie die Verwendung des MIPS32-Befehlssatzes in Betracht ziehen – aus irgendeinem seltsamen Grund scheinen viele Leute ihn leichter zu verstehen als x86-Assembly zu finden.

.

345690cookie-checki++ weniger effizient als ++i, wie kann man das zeigen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy