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:
1
intmain(intargc,char*argv[])
2
{
3
Timert1;
4
Timert2;
5
Timert3;
6
7
doubleduration1=0.0;
8
doubleduration2=0.0;
9
doubleduration3=0.0;
10
11
Objecto1;
12
Objecto2;
13
Objecto3;
14
15
for(inti=0;i<CALLS;i++)
16
{
17
Object2o;
18
19
t1.start();
20
21
o1.setParameter(o);
22
o=o1.getParameter();
23
24
t1.stop();
25
duration1+=t1.getDurationInSecs();
26
}
27
28
cout<<"Referenz: "<<duration1/CALLS<<endl;
29
30
for(inti=0;i<CALLS;i++)
31
{
32
Object2o;
33
34
t2.start();
35
36
o2.setRefParameter(o);
37
o2.getRefParameter(o);
38
39
t2.stop();
40
duration2+=t2.getDurationInSecs();
41
}
42
43
cout<<"Value: "<<duration2/CALLS<<endl;
44
45
for(inti=0;i<CALLS;i++)
46
{
47
Object2*o=newObject2();
48
49
t3.start();
50
51
o2.setPointerParameter(o);
52
o=o2.getPointerParameter();
53
54
t3.stop();
55
duration3+=t3.getDurationInSecs();
56
57
deleteo;
58
}
59
60
cout<<"Pointer: "<<duration3/CALLS<<endl;
61
62
Sleep(200000);
63
64
return0;
65
}
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
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.
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.
> 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.
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.
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
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:
1
voidObject::setRefParameter(Object2&o)
2
{
3
this->o=o;
4
}
5
6
voidObject::getRefParameter(Object2&o)
7
{
8
o=this->o;
9
}
10
11
voidObject::setParameter(Object2o)
12
{
13
this->o=o;
14
}
15
16
Object2Object::getParameter()
17
{
18
returno;
19
}
20
21
voidObject::setPointerParameter(Object2*o)
22
{
23
this->oPointer=o;
24
}
25
26
Object2*Object::getPointerParameter()
27
{
28
returnoPointer;
29
}
Wenn ihr noch weitere Tipps oder Erklärungen dafür habt wäre ich dankbar
;-)...
Vielen Dank!
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.
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.
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 :-)
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.
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!
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.
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.
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...
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.
1
classMyClass
2
{
3
public:
4
voiddoit()const;
5
...
6
};
7
8
voidfoo(MyClassa)
9
{
10
a.doit();
11
}
12
13
intmain()
14
{
15
MyClassb;
16
17
foo(b);
18
}
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
1
voidfoo(constMyClass&a)
2
{
3
...
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.
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
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.
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.
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.
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.
1
voidfoo(constirgendwas*value)
2
{
3
if(!value)
4
return;
5
6
irgendwaslocalCopy=*value;
7
8
// arbeite mit localCopy
9
localCopy.Member(5);// Objekt ändern
10
}
versus
1
voidfoo(irgendwaslocalCopy)
2
{
3
localCopy.Member(5);// Objekt ändern
4
}
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
....
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.
Hallo,
ich melde mich auch nochmal zu Wort!
Meine Zeitmessung ist anfürsich richtig gewesen. Ich messe zwar jetzt
die gesamte Schleife
1
t1.start();
2
3
for(inti=0;i<CALLS;i++)
4
{
5
o1.setParameter(oStack);
6
oStack=o1.getParameter();
7
}
8
9
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!