Wir haben die Frage, ob es einen Leistungsunterschied zwischen i++
und ++i
in C gibt .
Was ist die Antwort für C ++?
Wir haben die Frage, ob es einen Leistungsunterschied zwischen i++
und ++i
in C gibt .
Was ist die Antwort für C ++?
Antworten:
[Zusammenfassung: Verwenden ++i
Sie diese Option, wenn Sie keinen bestimmten Grund für die Verwendung haben i++
.]
Für C ++ ist die Antwort etwas komplizierter.
Wenn i
es sich um einen einfachen Typ handelt (keine Instanz einer C ++ - Klasse), gilt die Antwort für C ("Nein, es gibt keinen Leistungsunterschied") , da der Compiler den Code generiert.
Wenn jedoch i
ist eine Instanz einer C ++ Klasse, dann i++
und ++i
werden Anrufe zu einer der operator++
Funktionen. Hier ist ein Standardpaar dieser Funktionen:
Foo& Foo::operator++() // called for ++i
{
this->data += 1;
return *this;
}
Foo Foo::operator++(int ignored_dummy_value) // called for i++
{
Foo tmp(*this); // variable "tmp" cannot be optimized away by the compiler
++(*this);
return tmp;
}
Da der Compiler keinen Code generiert, sondern nur eine operator++
Funktion aufruft , gibt es keine Möglichkeit, die tmp
Variable und den zugehörigen Kopierkonstruktor zu optimieren . Wenn der Kopierkonstruktor teuer ist, kann dies erhebliche Auswirkungen auf die Leistung haben.
Ja. Es gibt.
Der ++ - Operator kann als Funktion definiert sein oder nicht. Für primitive Typen (int, double, ...) sind die Operatoren integriert, sodass der Compiler Ihren Code wahrscheinlich optimieren kann. Bei einem Objekt, das den ++ - Operator definiert, sind die Dinge jedoch anders.
Die Funktion operator ++ (int) muss eine Kopie erstellen. Dies liegt daran, dass von postfix ++ erwartet wird, dass es einen anderen Wert als den darin enthaltenen zurückgibt: Es muss seinen Wert in einer temporären Variablen enthalten, seinen Wert erhöhen und den temporären Wert zurückgeben. Im Fall von Operator ++ (), Präfix ++, muss keine Kopie erstellt werden: Das Objekt kann sich selbst inkrementieren und sich dann einfach selbst zurückgeben.
Hier ist eine Illustration des Punktes:
struct C
{
C& operator++(); // prefix
C operator++(int); // postfix
private:
int i_;
};
C& C::operator++()
{
++i_;
return *this; // self, no copy created
}
C C::operator++(int ignored_dummy_value)
{
C t(*this);
++(*this);
return t; // return a copy
}
Jedes Mal, wenn Sie operator ++ (int) aufrufen, müssen Sie eine Kopie erstellen, und der Compiler kann nichts dagegen tun. Wenn Sie die Wahl haben, verwenden Sie den Operator ++ (). Auf diese Weise speichern Sie keine Kopie. Dies kann bei vielen Inkrementen (große Schleife?) Und / oder großen Objekten von Bedeutung sein.
C t(*this); ++(*this); return t;
In der zweiten Zeile erhöhen Sie den Zeiger nach rechts. Wie wird also t
aktualisiert, wenn Sie dies erhöhen? Wurden die Werte nicht bereits kopiert t
?
The operator++(int) function must create a copy.
Nein ist es nicht. Nicht mehr Exemplare alsoperator++()
Hier ist ein Benchmark für den Fall, dass sich Inkrementoperatoren in verschiedenen Übersetzungseinheiten befinden. Compiler mit g ++ 4.5.
Ignorieren Sie vorerst die Stilprobleme
// a.cc
#include <ctime>
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
int main () {
Something s;
for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
std::clock_t a = clock();
for (int i=0; i<1024*1024*30; ++i) ++s;
a = clock() - a;
for (int i=0; i<1024*1024*30; ++i) s++; // warm up
std::clock_t b = clock();
for (int i=0; i<1024*1024*30; ++i) s++;
b = clock() - b;
std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
<< ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
return 0;
}
// b.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
for (auto it=data.begin(), end=data.end(); it!=end; ++it)
++*it;
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
Ergebnisse (Zeitangaben in Sekunden) mit g ++ 4.5 auf einer virtuellen Maschine:
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 1.70 2.39
-DPACKET_SIZE=50 -O3 0.59 1.00
-DPACKET_SIZE=500 -O1 10.51 13.28
-DPACKET_SIZE=500 -O3 4.28 6.82
Nehmen wir nun die folgende Datei:
// c.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
Es macht nichts in der Inkrementierung. Dies simuliert den Fall, dass die Inkrementierung eine konstante Komplexität aufweist.
Die Ergebnisse variieren jetzt extrem:
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 0.05 0.74
-DPACKET_SIZE=50 -O3 0.08 0.97
-DPACKET_SIZE=500 -O1 0.05 2.79
-DPACKET_SIZE=500 -O3 0.08 2.18
-DPACKET_SIZE=5000 -O3 0.07 21.90
Wenn Sie den vorherigen Wert nicht benötigen, machen Sie es sich zur Gewohnheit, das Vorinkrement zu verwenden. Seien Sie auch mit eingebauten Typen konsistent, Sie werden sich daran gewöhnen und laufen nicht Gefahr, unnötige Leistungseinbußen zu erleiden, wenn Sie jemals einen eingebauten Typ durch einen benutzerdefinierten Typ ersetzen.
i++
sagt increment i, I am interested in the previous value, though
.++i
sagt increment i, I am interested in the current value
oder increment i, no interest in the previous value
. Auch hier werden Sie sich daran gewöhnen, auch wenn Sie es gerade nicht sind.Vorzeitige Optimierung ist die Wurzel allen Übels. Da ist vorzeitige Pessimisierung.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
Tiefenüberquerung, sodass Sie sich nicht um die tatsächliche Baumstruktur (BSP, kd, Quadtree, Octree Grid usw.) kümmern müssen. Eine solche würde Iterator braucht etwas Zustand zu halten, zum Beispiel parent node
, child node
, index
und solche Sachen. Alles in allem ist meine Haltung, auch wenn es nur wenige Beispiele gibt, ...
Es ist nicht ganz richtig zu sagen, dass der Compiler die temporäre Variablenkopie im Postfix-Fall nicht optimieren kann. Ein schneller Test mit VC zeigt, dass dies zumindest in bestimmten Fällen möglich ist.
Im folgenden Beispiel ist der generierte Code beispielsweise für Präfix und Postfix identisch:
#include <stdio.h>
class Foo
{
public:
Foo() { myData=0; }
Foo(const Foo &rhs) { myData=rhs.myData; }
const Foo& operator++()
{
this->myData++;
return *this;
}
const Foo operator++(int)
{
Foo tmp(*this);
this->myData++;
return tmp;
}
int GetData() { return myData; }
private:
int myData;
};
int main(int argc, char* argv[])
{
Foo testFoo;
int count;
printf("Enter loop count: ");
scanf("%d", &count);
for(int i=0; i<count; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
}
Unabhängig davon, ob Sie ++ testFoo oder testFoo ++ ausführen, erhalten Sie immer noch den gleichen resultierenden Code. In der Tat, ohne die Zählung vom Benutzer einzulesen, brachte der Optimierer das Ganze auf eine Konstante. Also das:
for(int i=0; i<10; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
Daraus resultierte Folgendes:
00401000 push 0Ah
00401002 push offset string "Value: %d\n" (402104h)
00401007 call dword ptr [__imp__printf (4020A0h)]
Während es sicherlich der Fall ist, dass die Postfix-Version langsamer sein könnte, kann es durchaus sein, dass das Optimierungsprogramm gut genug ist, um die temporäre Kopie zu entfernen, wenn Sie sie nicht verwenden.
Im Google C ++ Style Guide heißt es:
Vorinkrement und Vorkrementierung
Verwenden Sie das Präfixformular (++ i) der Inkrement- und Dekrementoperatoren mit Iteratoren und anderen Vorlagenobjekten.
Definition: Wenn eine Variable inkrementiert (++ i oder i ++) oder dekrementiert (--i oder i--) wird und der Wert des Ausdrucks nicht verwendet wird, muss entschieden werden, ob vor- oder nachkrementiert (dekrementiert) werden soll.
Vorteile: Wenn der Rückgabewert ignoriert wird, ist das "Pre" -Formular (++ i) nie weniger effizient als das "Post" -Formular (i ++) und häufig effizienter. Dies liegt daran, dass nach dem Inkrementieren (oder Dekrementieren) eine Kopie von i erstellt werden muss. Dies ist der Wert des Ausdrucks. Wenn ich ein Iterator oder ein anderer nicht skalarer Typ bin, kann das Kopieren teuer sein. Da sich die beiden Inkrementtypen beim Ignorieren des Werts gleich verhalten, warum nicht einfach immer vorinkrementieren?
Nachteile: In C entwickelte sich die Tradition, Post-Inkrement zu verwenden, wenn der Ausdruckswert nicht verwendet wird, insbesondere in for-Schleifen. Einige finden Post-Inkrement leichter zu lesen, da das "Subjekt" (i) dem "Verb" (++) vorausgeht, genau wie in Englisch.
Entscheidung: Für einfache skalare (Nicht-Objekt-) Werte gibt es keinen Grund, eine Form zu bevorzugen, und wir erlauben dies auch. Verwenden Sie für Iteratoren und andere Vorlagentypen das Vorinkrement.
Ich möchte auf einen ausgezeichneten Beitrag von Andrew Koenig zu Code Talk in letzter Zeit hinweisen.
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
In unserem Unternehmen verwenden wir auch die Konvention von ++ iter, um gegebenenfalls Konsistenz und Leistung zu gewährleisten. Aber Andrew wirft übersehene Details in Bezug auf Absicht und Leistung auf. Es gibt Zeiten, in denen wir iter ++ anstelle von ++ iter verwenden möchten.
Entscheiden Sie also zuerst über Ihre Absicht, und wenn Pre oder Post keine Rolle spielen, entscheiden Sie sich für Pre, da dies einen gewissen Leistungsvorteil bietet, indem Sie vermeiden, dass ein zusätzliches Objekt erstellt und geworfen wird.
@ Ketan
... wirft übersehene Details in Bezug auf Absicht und Leistung auf. Es gibt Zeiten, in denen wir iter ++ anstelle von ++ iter verwenden möchten.
Offensichtlich haben Post und Pre-Inkrement unterschiedliche Semantiken, und ich bin sicher, alle sind sich einig, dass Sie bei Verwendung des Ergebnisses den entsprechenden Operator verwenden sollten. Ich denke, die Frage ist, was man tun soll, wenn das Ergebnis verworfen wird (wie in for
Schleifen). Die Antwort auf diese Frage (IMHO) lautet: Da die Leistungsaspekte bestenfalls vernachlässigbar sind, sollten Sie das tun, was natürlicher ist. Für mich ++i
ist das natürlicher, aber meine Erfahrung zeigt mir, dass ich in einer Minderheit bin und die Verwendung i++
für die meisten Leute, die Ihren Code lesen, weniger Metallaufwand verursacht .
Schließlich heißt die Sprache deshalb nicht " ++C
". [*]
[*] Fügen Sie eine obligatorische Diskussion über ++C
einen logischeren Namen ein.
Wenn der Rückgabewert nicht verwendet wird, wird garantiert, dass der Compiler im Fall von ++ i kein temporäres Element verwendet . Nicht garantiert schneller, aber garantiert nicht langsamer.
Bei Verwendung des Rückgabewerts ermöglicht i ++ dem Prozessor, sowohl das Inkrement als auch die linke Seite in die Pipeline zu verschieben, da sie nicht voneinander abhängig sind. ++ Ich kann die Pipeline blockieren, da der Prozessor die linke Seite erst starten kann, wenn sich die Vorinkrementierungsoperation vollständig durchgeschlichen hat. Auch hier ist ein Pipeline-Stillstand nicht garantiert, da der Prozessor möglicherweise andere nützliche Dinge zum Einstecken findet.
Mark: Ich wollte nur darauf hinweisen, dass Operator ++ gute Kandidaten für Inline sind, und wenn der Compiler dies wählt, wird die redundante Kopie in den meisten Fällen eliminiert. (zB POD-Typen, die normalerweise Iteratoren sind.)
Trotzdem ist es in den meisten Fällen immer noch besser, ++ iter zu verwenden. :-)
Der Leistungsunterschied zwischen ++i
und i++
wird deutlicher, wenn Sie Operatoren als wertrückgebende Funktionen betrachten und wie sie implementiert werden. Um das Verständnis zu erleichtern, werden die folgenden Codebeispiele so verwendet, int
als wäre es einstruct
.
++i
erhöht die Variable und gibt dann das Ergebnis zurück. Dies kann direkt und mit minimaler CPU-Zeit erfolgen, wobei in vielen Fällen nur eine Codezeile erforderlich ist:
int& int::operator++() {
return *this += 1;
}
Das Gleiche kann man aber nicht sagen i++
.
Nach dem Inkrementieren i++
wird häufig als Rückgabe des ursprünglichen Werts vor dem Inkrementieren angesehen. Eine Funktion kann jedoch nur dann ein Ergebnis zurückgeben, wenn es fertig ist . Infolgedessen ist es erforderlich, eine Kopie der Variablen mit dem ursprünglichen Wert zu erstellen, die Variable zu erhöhen und dann die Kopie mit dem ursprünglichen Wert zurückzugeben:
int int::operator++(int& _Val) {
int _Original = _Val;
_Val += 1;
return _Original;
}
Wenn es keinen funktionalen Unterschied zwischen Pre-Inkrement und Post-Inkrement gibt, kann der Compiler eine Optimierung durchführen, sodass zwischen beiden kein Leistungsunterschied besteht. Wenn jedoch ein zusammengesetzter Datentyp wie ein struct
oder class
beteiligt ist, wird der Kopierkonstruktor nach dem Inkrementieren aufgerufen, und es ist nicht möglich, diese Optimierung durchzuführen, wenn eine tiefe Kopie erforderlich ist. Daher ist das Vorinkrementieren im Allgemeinen schneller und erfordert weniger Speicher als das Nachinkrementieren.
@Mark: Ich habe meine vorherige Antwort gelöscht, weil es ein bisschen umgedreht war, und habe allein dafür eine Ablehnung verdient. Ich denke tatsächlich, dass es eine gute Frage in dem Sinne ist, dass sie fragt, was viele Leute denken.
Die übliche Antwort ist, dass ++ i schneller ist als i ++, und zweifellos ist es das, aber die größere Frage lautet: "Wann sollte es dich interessieren?"
Wenn der Anteil der CPU-Zeit, die für das Inkrementieren von Iteratoren aufgewendet wird, weniger als 10% beträgt, ist dies möglicherweise egal.
Wenn der Anteil der CPU-Zeit, die für das Inkrementieren von Iteratoren aufgewendet wird, mehr als 10% beträgt, können Sie überprüfen, welche Anweisungen diese Iteration ausführen. Überprüfen Sie, ob Sie nur Ganzzahlen erhöhen können, anstatt Iteratoren zu verwenden. Die Chancen stehen gut, dass Sie dies könnten, und obwohl dies in gewissem Sinne weniger wünschenswert ist, stehen die Chancen gut, dass Sie im Wesentlichen die gesamte Zeit sparen, die Sie in diesen Iteratoren verbringen.
Ich habe ein Beispiel gesehen, bei dem das Iterator-Inkrementieren weit über 90% der Zeit in Anspruch nahm. In diesem Fall reduzierte das Inkrementieren von Ganzzahlen die Ausführungszeit im Wesentlichen um diesen Betrag. (dh besser als 10x Beschleunigung)
@ Wilhelmtell
Der Compiler kann das Temporäre entfernen. Wörtlich aus dem anderen Thread:
Der C ++ - Compiler kann stapelbasierte Provisorien entfernen, auch wenn dies das Programmverhalten ändert. MSDN-Link für VC 8:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
Ein Grund, warum Sie ++ i auch bei integrierten Typen verwenden sollten, bei denen es keinen Leistungsvorteil gibt, besteht darin, sich eine gute Angewohnheit anzueignen.
Beide sind so schnell;) Wenn Sie möchten, dass es die gleiche Berechnung für den Prozessor ist, ist es nur die Reihenfolge, in der es gemacht wird, die sich unterscheidet.
Zum Beispiel der folgende Code:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Stellen Sie die folgende Baugruppe her:
0x0000000100000f24 <main+0>: push %rbp 0x0000000100000f25 <main+1>: mov %rsp,%rbp 0x0000000100000f28 <main+4>: movl $0x0,-0x4(%rbp) 0x0000000100000f2f <main+11>: incl -0x4(%rbp) 0x0000000100000f32 <main+14>: movl $0x0,-0x8(%rbp) 0x0000000100000f39 <main+21>: incl -0x8(%rbp) 0x0000000100000f3c <main+24>: mov $0x0,%eax 0x0000000100000f41 <main+29>: leaveq 0x0000000100000f42 <main+30>: retq
Sie sehen, dass es für a ++ und b ++ eine inkl. Mnemonik ist, also ist es die gleiche Operation;)
Bei der beabsichtigten Frage ging es darum, wann das Ergebnis nicht verwendet wird (das geht aus der Frage für C hervor). Kann jemand dies beheben, da die Frage "Community-Wiki" ist?
Über vorzeitige Optimierungen wird häufig Knuth zitiert. Das stimmt. aber Donald Knuth würde damit niemals den schrecklichen Code verteidigen, den man heutzutage sehen kann. Schon mal a = b + c unter Java Integers (nicht int) gesehen? Das sind 3 Boxing / Unboxing-Conversions. Solche Dinge zu vermeiden ist wichtig. Und nutzloses Schreiben von i ++ anstelle von ++ i ist der gleiche Fehler. EDIT: Wie Phresnel in einem Kommentar schön formuliert, kann dies als "vorzeitige Optimierung ist böse, ebenso wie vorzeitige Pessimisierung" zusammengefasst werden.
Sogar die Tatsache, dass Menschen eher an i ++ gewöhnt sind, ist ein unglückliches C-Erbe, das durch einen konzeptionellen Fehler von K & R verursacht wurde (wenn Sie dem Vorsatzargument folgen, ist das eine logische Schlussfolgerung; und die Verteidigung von K & R, weil sie K & R sind, ist bedeutungslos, sie sind bedeutungslos großartig, aber sie sind nicht großartig als Sprachdesigner; es gibt unzählige Fehler im C-Design, die von get () über strcpy () bis zur strncpy () -API reichen (sie sollte seit Tag 1 die strlcpy () -API haben). ).
Übrigens bin ich einer von denen, die nicht genug an C ++ gewöhnt sind, um ++ zu finden. Ich bin nervig zu lesen. Trotzdem benutze ich das, da ich anerkenne, dass es richtig ist.
++i
ärgerlicher als i++
(tatsächlich fand ich es cooler), aber der Rest Ihres Beitrags erhält meine volle Bestätigung. Vielleicht einen Punkt hinzufügen "Vorzeitige Optimierung ist böse, ebenso wie vorzeitige Pessimisierung"
strncpy
diente einem Zweck in den Dateisystemen, die sie zu der Zeit verwendeten; Der Dateiname war ein 8-stelliger Puffer und musste nicht nullterminiert werden. Sie können ihnen nicht die Schuld geben, dass sie 40 Jahre in der Zukunft der Sprachentwicklung nicht gesehen haben.
strlcpy()
wurde durch die Tatsache gerechtfertigt, dass es noch nicht erfunden worden war.
Zeit, den Leuten Juwelen der Weisheit zu vermitteln;) - Es gibt einen einfachen Trick, mit dem sich das C ++ - Postfix-Inkrement so ziemlich wie das Präfix-Inkrement verhält (Ich habe es für mich selbst erfunden, aber ich habe es auch im Code anderer Leute gesehen, also bin ich es nicht allein).
Grundsätzlich besteht der Trick darin, die Hilfsklasse zu verwenden, um das Inkrement nach der Rückkehr zu verschieben, und RAII kommt zur Rettung
#include <iostream>
class Data {
private: class DataIncrementer {
private: Data& _dref;
public: DataIncrementer(Data& d) : _dref(d) {}
public: ~DataIncrementer() {
++_dref;
}
};
private: int _data;
public: Data() : _data{0} {}
public: Data(int d) : _data{d} {}
public: Data(const Data& d) : _data{ d._data } {}
public: Data& operator=(const Data& d) {
_data = d._data;
return *this;
}
public: ~Data() {}
public: Data& operator++() { // prefix
++_data;
return *this;
}
public: Data operator++(int) { // postfix
DataIncrementer t(*this);
return *this;
}
public: operator int() {
return _data;
}
};
int
main() {
Data d(1);
std::cout << d << '\n';
std::cout << ++d << '\n';
std::cout << d++ << '\n';
std::cout << d << '\n';
return 0;
}
Erfunden wurde für einige schwere benutzerdefinierte Iterator-Codes und verkürzt die Laufzeit. Die Kosten für Präfix und Postfix sind jetzt eine Referenz, und wenn dies ein benutzerdefinierter Operator ist, der sich stark bewegt, haben Präfix und Postfix für mich dieselbe Laufzeit ergeben.
++i
ist schneller als i++
weil es keine alte Kopie des Wertes zurückgibt.
Es ist auch intuitiver:
x = i++; // x contains the old value of i
y = ++i; // y contains the new value of i
In diesem C-Beispiel wird "02" anstelle der erwarteten "12" gedruckt:
#include <stdio.h>
int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}