Datum:
Hallo, ich habe eine Frage an euch... Folgendes Problem: Bei der Laufzeitanalyse einer Parameterübergabe (lesen und schreiben) habe ich in einem kleinen C++ Testprogramm Laufzeiten ermittelt (Timerauflösung bei ca. 0,580 ns). Unterschieden wird zwischen Referenzen, Values und Pointern. Es wurde für jede dieser Übergabemöglichkeiten in einer Schleife (je 5.000.000 Durchläufe) einem Objekt ein anderes Objekt hinzugefügt und wieder ausgelesen. Die Laufzeiten der Parameterübergabe und den Methodenaufrufen werden anschließend noch gemittelt. Nur zum besseren Verständnis erst mal der Quellcode:
int main(int argc, char* argv[]) { Timer t1; Timer t2; Timer t3; double duration1 = 0.0; double duration2 = 0.0; double duration3 = 0.0; Object o1; Object o2; Object o3; for(int i = 0; i < CALLS; i++) { Object2 o; t1.start(); o1.setParameter(o); o = o1.getParameter(); t1.stop(); duration1 += t1.getDurationInSecs(); } cout << "Referenz: " << duration1/CALLS << endl; for(int i = 0; i < CALLS; i++) { Object2 o; t2.start(); o2.setRefParameter(o); o2.getRefParameter(o); t2.stop(); duration2 += t2.getDurationInSecs(); } cout << "Value: " << duration2/CALLS << endl; for(int i = 0; i < CALLS; i++) { Object2 *o = new Object2(); t3.start(); o2.setPointerParameter(o); o = o2.getPointerParameter(); t3.stop(); duration3 += t3.getDurationInSecs(); delete o; } cout << "Pointer: " << duration3/CALLS << endl; Sleep(200000); return 0; } |
Das Objekt "Object" enthält lediglich ein Stack- und ein Heap-Objekt von "Object2". Das "Object2" enthält keinerlei Daten. Die Laufzeiten betragen. Referenz: 514,8 ns Pointer: 543 ns Value: 514,5 ns Nun zu meiner eigentlichen Frage: Wieso ist die Laufzeit bei der Übergabe eines Pointers so hoch. Ich hätte erwartet, dass die Laufzeiten in der Reihenfolge Value > Pointer > Referenz auftreten. Grüße, cFighter
Datum:
Mach es kompilierbar oder schau in den erzeugten Code. Auf C Ebene ist das Kaffeesatzlesen.
Datum:
vermutlich ist das Problem das anlegen des objects.
> Object2 *o = new Object2();
hier muss der Heap genutzt werden, bei den anderen liegt das objekt auf
dem Stack und immer an der gleichen stelle. Dies macht vermutlich ein
grossteil der Laufzeit aus.
Datum:
nachtrag: die Zeitmessung startet zwar danach, aber ich würde der Reihenfolge nicht trauen.
Datum:
Nicht nur das. In dieser Dimension darf man Effekte durch Caches aller Art nicht vernachlässigen. Daher sollte man mindestens den ersten Durchlauf eines Testcodes nicht mit in die Messung nehmen.
Datum:
> einem Objekt ein anderes Objekt hinzugefügt
Was heißt hinzugefügt?
Oder anders gefragt: wie sehen die einzelnen set und get Methoden aus?
Und ob das hier
t3.start();
o2.setPointerParameter(o);
o = o2.getPointerParameter();
t3.stop();
kummulativ eine halbwegs brauchbare Zeit ergibt, ist auch fraglich.
Stell dir vor du stoppst mit einer Kirchturmuhr den Carl Lewis auf
seinem 100 Meter Lauf. Da wird nichts vernünftiges rauskommen, egal wie
oft du die Messung wiederholst und die Einzelzeiten addierst. Bei 98 von
100 Versuchen wird die Zeit 0 sein und bei 2 dieser 100 Messungen hat
sich der Stundenzeiger weiterbewegt: Summe 2 Stunde. Die 100 Versuche
haben also 2 Stunde 'Zeit verbraucht'. Macht knapp 1.2 Minuten pro Lauf.
Das schaff sogar ich auf 100 Meter. Und ich bin kein Olympiasieger.
Datum:
Entweder compilieren ohne Optimierungen und dann den Assemblercode analysieren. Oder an deinem ganz konkreten Programm die Messungen machen. Alles andere ist wirklich Kaffeesatzlesen. Da sind einfach zu viele Nebeneffekte dabei.
Datum:
Hi, vielen Dank für eure Antworten. Die haben mir einige neue Erkenntnisse gebracht (nur leider war noch nicht die Lösung dabei die eine Erklärung dafür gibt). Nach einer neuen kleinen Messung mit dem Folgenden Quellcode
#include "stdafx.h" int main(int argc, char* argv[]) { Timer t1; Timer t2; Timer t3; Object o1; Object o2; Object o3; Object2 oStack; Object2 *oHeap = new Object2(); t1.start(); for(int i = 0; i < CALLS; i++) { o1.setParameter(oStack); oStack = o1.getParameter(); } t1.stop(); cout << "Referenz: " << t1.getDurationInSecs()/CALLS << endl; t2.start(); for(int i = 0; i < CALLS; i++) { o2.setRefParameter(oStack); o2.getRefParameter(oStack); } t2.stop(); cout << "Value: " << t2.getDurationInSecs()/CALLS << endl; t3.start(); for(int i = 0; i < CALLS; i++) { o2.setPointerParameter(oHeap); oHeap = o2.getPointerParameter(); } t3.stop(); cout << "Pointer: " << t3.getDurationInSecs()/CALLS << endl; delete oHeap; Sleep(200000); return 0; } |
kamen folgende Laufzeiten heraus: Referenz: 5,02857e-013 s Value: 5,02857e-013 s Pointer: 1,27072e-009 s Leider sieht es immer noch ähnlich aus. Meine Setter und Getter sehen folgendermaßen aus:
void Object::setRefParameter(Object2 &o) { this->o = o; } void Object::getRefParameter(Object2 &o) { o = this->o; } void Object::setParameter(Object2 o) { this->o = o; } Object2 Object::getParameter() { return o; } void Object::setPointerParameter(Object2 *o) { this->oPointer = o; } Object2* Object::getPointerParameter() { return oPointer; } |
Wenn ihr noch weitere Tipps oder Erklärungen dafür habt wäre ich dankbar ;-)... Vielen Dank!
Datum:
cFighter schrieb: > Referenz: 5,02857e-013 s Das sind 0.5 Pikosekunden pro Iteration. Respekt. Wenn der Prozessor also mindestens einen Takt pro Iteration benötigt, dann wird er mit mindestens 2000 Ghz getaktet.
Datum:
dir ist bewust das hier eine kopie von dem objekt gemacht wird, dies
kostet zeit!
void Object::setRefParameter(Object2 &o)
{
this->o = o;
}
wie ist du die zeitmessung implementiert? Sicher das du durch das double
nicht mehr fehler reinrechnest? Sotewas macht man lieber mit Bigint in
nanosekunden. (je nach Plaftform)
Mach doch mal bitte ein quellcode fertig wo alles drin steht, damit
können wir uns dann selber davon überzeugen.
Datum:
cFighter schrieb: > Wenn ihr noch weitere Tipps oder Erklärungen dafür habt wäre ich dankbar > ;-)... <Durch die Zähne pfeif> Der Optimizer ist besser als ich dachte :-)
Datum:
Peter II schrieb: > dir ist bewust das hier eine kopie von dem objekt gemacht wird, dies > kostet zeit! Bei einem leeren Objekt hält sich der Aufwand dafür in Grenzen. Sein Problem bei der letzten Messung könnte massgeblich damit zusammen hängen, dass der Optimizer klüger ist als der Programmier.
Datum:
Das ist mit ziemlicher Wahrscheinlichkeit die Timerauflösung die da zubuche schlägt! Aber leider ist die Parameterübergabe mittels Pointer immer noch Langsamer als die mit Value! Ich werde mal versuchen den Assemblercode zu verstehen! melde mich die Tage nocheinmal (vielleicht auch erst nächste Woche --> cFighter geht jetzt ins lange Wochenende!) Euch natürlich erst mal vielen Dank und auch ein schönes WE!
Datum:
cFighter schrieb: > Das ist mit ziemlicher Wahrscheinlichkeit die Timerauflösung die da > zubuche schlägt! Aber leider ist die Parameterübergabe mittels Pointer > immer noch Langsamer als die mit Value! Geh mal davon aus, dass deine Messwerte nicht stimmt. Davon jetzt irgendwas abzuleiten ist nicht angebracht. Deine Devise mit diesem Zahlenwerk muss lauten: Wer misst, misst viel Mist. > Referenz: 5,02857e-013 s > Value: 5,02857e-013 s > Pointer: 1,27072e-009 s Die e-009 könnten noch so einigermassen realistisch sein, obwohl mir der Wert auch noch um mindestens eine gute 10-er Potenz zu niedrig vorkommt. Aber die e-013 sind völlig unmöglich. Wie A.K. weiter oben schon schrieb: Selbst wenn wir alles zu Gunsten der CPU ins Feld führen und mehr oder weniger alles vernachlässigen, was da an Arbeit für den Rechner notwendig ist, müsste dein PC mit 2000 Ghz getaktet sein, damit ein Vorstoss in diesen Zeitbereich möglich wird. Und 2000 Ghz hast du ganz sicher nicht. Der Plausibilitätscheck sagt: Die Zahlen sind gewürfelt aber auf keinen Fall real.
Datum:
hmm 2000 GHz ? oder.......... vielleicht n GHz die operation nur einmal in n Schritten ausgeführt statt 2000mal?
Datum:
Eben. Stichwort: Optimizer. Siehe auch die bei Anfängern beliebten aber vom Compiler wegoptimierten Zeitschleifen.
Datum:
Es geht mehr ums Symptom an sich.
Bei hinreichend komplexem Code macht es speziell in C++ kaum mehr Sinn
Einzelaktionen mittels Timer auszumessen. Gerade C++ holt sich sehr viel
Speed aus Compileroptimierungen. Timing-Messungen machen da auf
algorithmischer Ebene Sinn. Einzeldinge auszumessen (wie lange dauert
ein for, wie lange der sinngleiche while, was ist schneller Pass per
Reference/Pass per Pointer macht meistens keinen Sinn. Wer denkt, er
könne so sein Programm wesentlich 'optimieren' hat meistens mit Zitronen
gehandelt. Überlass solche Dinge dem Compiler, der macht das
zuverlässig. Schaff ihm die Möglichkeit zum Optimieren (kleine
Funktionen inline in die Klassendefinition) und ansonsten kümmere dich
um die Algorithmen. Da ist mehr zu holen.
Mit einem Entscheidungsbaum
+ muss die Funktion das Argument beim Aufrufer ändern können?
|
+-- Ja:
| benutze eine Referenz
|
+-- Nein: muss der Aufrufer mitteilen können:
| Ich hab nichts für dich?
|
+-- Ja: benutze einen Pointer, wobei NULL genau diese Zusatzinfo
| darstellt.
|
+-- Nein: Handelt es sich um einen primitiven, eingebauten
| Datentyp?
|
+-- Ja: benutze pass per Object Copy
|
+-- Nein: muss die Funktion sowieso eine Kopie machen?
|
+-- Ja: benutze pass per Object Copy
|
+-- Nein: benutze eine const Referenz
(bzw. sinngemäss die Fälle, die ich hier vergessen habe ... Grundprinzip
ist: Pass per Referenz, es sei denn spezielle Fälle liegen vor)
Mit diesen Grundentscheidundungen kriegst du Parameterpassing, an dem du
nicht mehr viel drehen musst. Langsame Programme sind nicht deswegen
langsam, weil Pass per Pointer schneller/langsamer ist als Pass per
Reference.
Datum:
Hallo, entschuldigt meine lange Abwesenheit. Ich habe spontan noch eine andere, dringende Aufgabe bekommen. Nun zu meiner Messung: A. K. schrieb: > cFighter schrieb: > >> Referenz: 5,02857e-013 s > > Das sind 0.5 Pikosekunden pro Iteration. Respekt. Wenn der Prozessor > also mindestens einen Takt pro Iteration benötigt, dann wird er mit > mindestens 2000 Ghz getaktet. Hierbei beziehen sich die 5 Pikosekunden auf EINE Iteration! Ich messe zuerst die Gesamtdauer aller Iterationen und teile anschließend durch die Iterationsanzahl (im Quellcode CALLS = 5.000.000). Somit wird hier lediglich das Mittel eines sets und eines gets bestimmt. Vielleicht habe ich euch da ja auch falsch verstanden, dann bitte nochmal klarstellen. kbuchegg hat im letzten Beitrag geschrieben, dass es keinen Sinn macht die Parameterübergabe an Funktionen zu optimieren. Das sehe ich nicht ganz so. Da dieses Projekt mein erstes C++ Projekt ist habe ich zu beginn eine relativ hohe Laufzeit von ca. 200ms gehabt. Die Compileroptimierung hat da noch ca. 50% heraus holen können. Jetzt, nachdem ich temporäre Objekte und Parameterübergaben geändert bzw. entsorgt habe, habe ich eine Laufzeit von ca 5ms!!! Die temporären Objekte waren hierbei im Zusammenhang mit Pointern, welche an verschiedene Methoden übergeben wurden. Nun wollte ich aufgrund des krassen Unterschiedes (200ms zu 5ms) die Laufzeiteigenschaften der Parameterübergabe untersuchen. Ich hätte nie gedacht das das soooo viel aufwand ist. Den Assemblercode habe ich mir bisher noch nicht angesehen. Aber vielleicht begründe ich das Nutzen der Referenzen einfach damit, dass weniger temporäre Objekte genutzt werden wodurch die Laufzeit verbessert wurde! Wenn ihr noch eine einfache und bessere Idee habt könnt ihr gerne nochmal euren Senf dazu geben ;-) Vielen Dank euch erst mal...
Datum:
cFighter schrieb: >> Das sind 0.5 Pikosekunden pro Iteration. Respekt. Wenn der Prozessor >> also mindestens einen Takt pro Iteration benötigt, dann wird er mit >> mindestens 2000 Ghz getaktet. > > Hierbei beziehen sich die 5 Pikosekunden auf EINE Iteration! Ich messe > zuerst die Gesamtdauer aller Iterationen und teile anschließend durch > die Iterationsanzahl (im Quellcode CALLS = 5.000.000). Somit wird hier > lediglich das Mittel eines sets und eines gets bestimmt. > > Vielleicht habe ich euch da ja auch falsch verstanden, dann bitte > nochmal klarstellen. Dir scheint nicht klar zu sein, worauf das hinausläuft. Egal ob MIttelwert oder nicht. Rechnet man zurück, wie schnell dein Prozessor sein müsste um EINE derartige Instruktion zu machen, dann kommt kman drauf, dass der Prozessor ca 2000 GIGA-HERZ machen müsste. Was selbstverständlich kompletter Unsinn ist, denn KEIN Prozessor macht zur Zeit 2-tausend-Giga-Herz. Wir stehen erst technologisch bei ca 3 (drei). 2-tausend zu drei ist aber ein krasser Unterschied. Die einzige mögliche Erklärung dafür lautet: Deine Messung ist um einen Faktor von rund 1000 falsch und somit unbrauchbar! Wer misst, misst viel Mist. > kbuchegg hat im letzten Beitrag geschrieben, dass es keinen Sinn macht > die Parameterübergabe an Funktionen zu optimieren. Nun ja. Die der jeweilgen Situation angemessene Übergabemethode zu benutzen würde ich noch nicht als Optimierung bezeichnen. Du wirst ja schliesslich das Berechnen der Fläche eines Rechtecks (a: 8 Meter, b: 7 Meter) in der Form 8 * 7 und nicht mittels 8 + 8 + 8 + 8 + 8 + 8 + 8 auch nicht als 'Optimierung' bezeichnen. Oder doch? > Das sehe ich nicht > ganz so. > Da dieses Projekt mein erstes C++ Projekt ist habe ich zu beginn eine > relativ hohe Laufzeit von ca. 200ms gehabt. Die Compileroptimierung hat > da noch ca. 50% heraus holen können. Jetzt, nachdem ich temporäre > Objekte und Parameterübergaben geändert bzw. entsorgt habe, habe ich > eine Laufzeit von ca 5ms!!! Die temporären Objekte waren hierbei im > Zusammenhang mit Pointern, welche an verschiedene Methoden übergeben > wurden. Das du wissen musst, was du tust, habe ich erst mal vorausgesetzt. Wenn du natürlich haufenweise temporäre Objekte erzeugst, darfst du dich nicht wundern, wenn das langsam ist. Die beste Optimierung ist immer noch, sein Werkzeug (in dem Fall die Sprache C++) zu beherrschen. Und wenn du den Entscheidungsbaum ansiehst, wann welcher Übergabemechanismus angebracht ist, dann wirst du feststellen, dass Pass per Reference bevorzugt wird. Solange es keinen anderen Grund dagegen gibt, fährt man damit gut. > Nun wollte ich aufgrund des krassen Unterschiedes (200ms zu 5ms) die > Laufzeiteigenschaften der Parameterübergabe untersuchen. Ich hätte nie > gedacht das das soooo viel aufwand ist. Deine Zeiteinsparung kommt aber gar nicht aus dem Parameter Passing. Deine Codeeinsparung kommt aus der exzessiven Erzeugung von temporären Objekten. D.h. wenn du für die Zukunft etwas lernen willst, dann lerne wann, wo und wie du sinnlos temporäre Objekte erzeugt hast.
class MyClass
{
public:
void doit() const;
...
};
void foo( MyClass a )
{
a.doit();
}
int main()
{
MyClass b;
foo( b );
}
|
Q: Was sagt der Entscheidungsbaum von oben zu dieser Situation? A: Er sagt: pass per const Reference. Q: Was liegt tatsächlich vor? A: Es liegt Pass per Copy vor. Q: Ist das sinnvoll? A: Nein, ist es nicht. Es gibt keinen Grund, warum in dieser Situation eine Objektkopie gemacht werden müsste. Daher ist Pass per Copy nicht sinnvoll. Pass per Reference hätte die Kopie vermieden. Q: War daher das erzeugen eines 'temporären' Objektes sinnvoll? A: Nein, war es nicht. Q: Was könnte man tun? A: Man könnte das machen, was sich aus dem Entscheidungsbaum bzw. aus der Daumenregel "Bevorzuge eine Referenz, wenn es keine Gründe dagegen gibt" ergibt und abändern
void foo( const MyClass& a ) { ... |
OK, wenn du das als "Optimierung" ansehen willst, kannst du das natürlich gerne tun. Ich würde das aber eher in die Kategorie "sein Handwerkszeug richtig anwenden können" einordnen.
Datum:
Jetzt habe ich das mit den 2000 GHz verstanden! Dann werde ich die Laufzeit (da sie sowieso mit Optimierung im Pikosekunden bereich liegt) vernachlässigen. Den Assemblercode habe ich mit gerade angesehen, wobei hier auch meine Vermutung der Laufzeit bestätig wird. (Value > Pointer > Referenz) --> sofern der Entscheidungsbaun erst mal egal ist. Trotz dem euch allen vielen Dank für die Unterstützung
Datum:
cFighter schrieb: > bestätig wird. (Value > Pointer > Referenz) --> sofern der > Entscheidungsbaun erst mal egal ist. Genau das ist aber der entscheidende Punkt: Die Entscheidung ob Value, Pointer oder Referenz macht man davon abhängig, welches Verhalten die Funktionsschnittstelle implementieren muss und nicht davon, was davon schneller ist. Denn wenn du eigentlich einen Pass per Value brauchen würdest, stattdessen aber aus Laufzeitgründen einen Pointer übergibst, dann ist zwar das Parameterpassing tatsächlich schneller. Allerdings wenn wir jetzt alles zusammenrechnen, bis du mit dem Pointer-Passing wieder funktional auf gleich mit dem Value-Passing bist, ist es in Summe langsamer. Der Keller eines Hauses ist doppelt so schnell gemauert, wenn man nur jeden 2.ten Ziegelstein einbaut. Schon. Nur wenn dann letztendes das Dach aufgesetzt werden soll, sind umfangreiche Sanierungsmassnahmen notwendig, sonst stürzt der Keller ein. -> In Summe ist man mit "jeden 2.ten Ziegel weglassen" also nicht schneller, sondern langsamer. Selbst wenn die Maurer beim Keller-Mauern in 2 Tagen statt in 4 fertig waren.
Datum:
Karl Heinz Buchegger schrieb: > Die Entscheidung ob Value, Pointer oder Referenz macht man davon > abhängig, welches Verhalten die Funktionsschnittstelle implementieren > muss und nicht davon, was davon schneller ist. Ja, soweit es um per value einerseits oder pointer/Referenz andererseits geht. Das unterscheidet sich grundlegend funktional (und hinsichtlich Laufzeit). Zeiger gegenüber Referenz ist dagegen i.d.R. nur noch Geschmacksfrage, weil in vielen Fällen beide gleichwertig sind (außer 1. wenn Zeiger nötig sind wg. C-Kompatibilität, oder 2. der Zeiger auf "ungültig", also NULL gesetzt werden können soll). Wobei es natürlich besseren oder schlechteren Geschmak gibt... Wenn hier für Zeiger gegenüber Referenz deutliche Laufzeitunterschiede gemessen werden, dann ist die Messung falsch.
Datum:
Karl Heinz Buchegger schrieb: > Denn wenn du eigentlich einen Pass per Value brauchen würdest, > stattdessen aber aus Laufzeitgründen einen Pointer übergibst, dann ist > zwar das Parameterpassing tatsächlich schneller. Allerdings wenn wir > jetzt alles zusammenrechnen, bis du mit dem Pointer-Passing wieder > funktional auf gleich mit dem Value-Passing bist, ist es in Summe > langsamer. Hast du dafür Belege? Das halte ich für ein Gerücht. warum soll die Übergabe von dem und Arbeit mit einem Pointer genauso langsam sein, wie das Kopieren von und Arbeiten mit dem Objektes. Die Arbeit mit beidem innerhalb der Funktion sollte ziemlich gleich schnell gehen, da die Member des Objektes in beiden Fällen gleich adressiert werden, nur das bei dem Einen ein [Pointer + Memberoffset] und bei dem anderen ein [Stackbasispointer + Memberoffset] ausgelesen wird. bleibt also noch der Kopiervorgang. und der ist langsamer sobald die Strukturgröße die Größe des Datentyps 'Pointer' überschreitet.
Datum:
Vlad Tepesch schrieb: > Karl Heinz Buchegger schrieb: >> Denn wenn du eigentlich einen Pass per Value brauchen würdest, >> stattdessen aber aus Laufzeitgründen einen Pointer übergibst, dann ist >> zwar das Parameterpassing tatsächlich schneller. Allerdings wenn wir >> jetzt alles zusammenrechnen, bis du mit dem Pointer-Passing wieder >> funktional auf gleich mit dem Value-Passing bist, ist es in Summe >> langsamer. > > Hast du dafür Belege? Das halte ich für ein Gerücht.
void foo( const irgendwas * value ) { if( !value ) return; irgendwas localCopy = *value; // arbeite mit localCopy localCopy.Member( 5 ); // Objekt ändern } |
versus
void foo( irgendwas localCopy ) { localCopy.Member( 5 ); // Objekt ändern } |
Das wird sich um nichts reißen.
Ich denke du hast diesen Satzteil
> Denn wenn du eigentlich einen Pass per Value brauchen würdest
überlesen oder falsch interpretiert oder ich hab mich schlecht
ausgedrückt.
Die Funktion braucht eine lokale Kopie (aus welchen Gründen auch immer).
Ob diese Kopie in der Funktion hergestellt wird oder beim Parameter
Passing, ist laufzeitmässig egal. Bleibt noch der zusätzliche Pointer
....
Datum:
Karl Heinz Buchegger schrieb: > Ich denke du hast diesen Satzteil >> Denn wenn du eigentlich einen Pass per Value brauchen würdest > überlesen oder falsch interpretiert oder ich hab mich schlecht > ausgedrückt. Ja ich glaub, ich hab dich misverstanden.
Datum:
Hallo, ich melde mich auch nochmal zu Wort! Meine Zeitmessung ist anfürsich richtig gewesen. Ich messe zwar jetzt die gesamte Schleife
t1.start(); for(int i = 0; i < CALLS; i++) { o1.setParameter(oStack); oStack = o1.getParameter(); } t1.stop(); |
aber die Zeiten sind jetzt endlich interpretierbar! Value: 0,210425 s Referenz: 0,122376 s Pointer: 0,141203 s Das Problem worauf keiner von euch gekommen ist: Die Messung meiner Zeiten beginnt mit dem Value-passing, jedoch gebe ich leider einen Text aus der die Zeiten als Referenz-Passing definiert! (sieht man oben im Code besser!) Naja, trotz dem habe ich viele euren Kommentaren gelernt. Vielen Dank!