C GCC Code Optimierungen Code Verschiebung verhindern

Dabei seit
Nov. 2007
Beiträge
671
#1
Hallo Zusammen,

ich habe folgendes Problem: Ich möchte gerne die Zeit messen, die ein Programmteil, welcher in C geschrieben ist, zur Berechnung braucht. Da ich das ganze auf einem FPGA Board unter Linux mache, gestaltet sich dies alles leider nicht so einfach. Die Clock ist auf 10 Millisekunden eingestellt, sodass ich damit nicht genauer messen kann (in ns zb). Nun habe ich per Software bei der benutzten Hardware die Möglichkeit per Registerzugriff die Cycles zu messen und daraus mir meine ns zu berechnen. Wenn ich mir den ObjDump anschaue, sieht der für mich eigentlich normal aus. Ich versuche nur eine einfach Addition zu messen komme aber auf mehr als ~6000 cycles, was dafür viel zu viel ist, auch wenn Cache etc noch nicht aufgewärmt ist. Die CPU ist eine SPARC Architektur von Typ LEON3. Nun hab ich den Verdacht, dass der Compiler Gcc mir irgendwie den Code optimiert bzw. vielleicht in die Zeitmessung schiebt wodurch eine solch hohe Cycle-Zahl entsteht. Leider habe ich bisher keine andere Möglichkeit gefunden, genauer zu messen.

Ich habe schon per timer_list im proc Verzeichnis geschaut und mir ist dort hrtimer aufgefallen. Kann man den irgendwie auch im Userspace benutzen?
 

Cordless

Lieutenant
Dabei seit
Dez. 2004
Beiträge
546
#2
Compiliere doch einfach mit -S und -g und schau dir den generierten Assemblercode an, dann weißt du sofort ob Code verschoben wurde.
 

slashmaxx

Cadet 3rd Year
Dabei seit
Okt. 2006
Beiträge
47
#4
So wie ich dich verstanden habe, benutzt du quasi den "Debugger" der unter Linux läuft, um auf dem FPGA per Registerzugriff einen Wert auszulesen. Zählt das Register wirklich die Zeit? Wie genau löst du die Zeitmessung aus? Hier wären ne ganze Reihe an Fragen, die ich aber nicht stellen werde.

Stattdessen kann ich dir eine "old school" Möglichkeit aufzeigen (in Pseudo-Code), wo man diverse Debugger- und Tracereinflüsse nicht hat und sehr präzise de realen Wert erhält.

Code:
SetIO();

Addition();

ClearIO();
Hier wird die Veränderung eines beliebigen Messpins per Oszilloskop gemessen. Der Messpin wird per SetIO()und ClearIO() gesetzt bzw. gelöscht. Die Zeit die du misst, merkst du dir. Damit du eine bereinigte Zeit erhälst, muss du noch einmal ohne "Addition()" messen und diese Zeit von der zuerst Gemessenen abziehen.

Nun kann man behaupten (und das strenggenommen zurecht), dass der Code, dessen Zeitwert man abzieht, nicht vergleichbar ist mit dem Coder mit der "Addition()", weil der Compiler ja einen "ganz" anderen Maschinencode generiert. Hier kann man aber eigentlich von ausgehen, das die Codeunterschiede die du mit und ohne Addition erhalten wirst minimal sind und keinen signifikanten Einfluss haben werden.

Gruß slash
 
Zuletzt bearbeitet:
Dabei seit
Dez. 2011
Beiträge
203
#6
Stattdessen kann ich dir eine "old school" Möglichkeit aufzeigen (in Pseudo-Code), wo man diverse Debugger- und Tracereinflüsse nicht hat und sehr präzise de realen Wert erhält.
old-school ist gut :).

Es löst aber nicht das Problem der sehr groben Zeitauflösung, welche ev. >> all deine Codezeilen zusammen ist.

Ich kenne diesen +- plattformunabhängigen old-school Trick:

Code:
int nIter = 1000000; // auf einen für das Problem sinnvollen Wert setzen

startTimeMeasurment();

for (int i = 0; i < nIter;++i)
  callFunc();

stopTimeMeasurment();
und callFunc schaut einmal so aus:

Code:
int callFunc(int arg1, double arg2)
{ return 0; }
und einmal so:

Code:
int callFunc(int arg1, double arg2)
{
  // code to measure goes in here

return 0;
}

Man lässt das nun für beide Varianten von callFunc durchlaufen und misst jeweils die Zeit -> die Nettozeit des zu messenden Codes sollte sich dann als Differenz der Durchläufe ergeben. Natürlich sollte nIter so gewählt werden, dass die gesamte Laufzeit >> der Zeitauflösung der Maschine ist.
Der Vorteil ist, dass aller overhead, von Zeitmessung über looping bis function call overheads (incl. parameter-passing), exakt rausgerechnet werden müsste.

Das Problem in der Praxis ist den Compiler so auszutricksen, dass der den Aufruf der nichtstuenden callFunc (oder auch der anderen, je nachdem was die berechnet) nicht als unnötigen Funktionsaufruf erkennt und somit (ev. samt ganzer Schleife) schlicht wegoptimiert (= nicht exekutiert). In C++ hilft wenn callFunc eine virtuelle Funktion ist, oder in C/C++ wenn was in eine volatile Variable reingeschrieben wird, aber man muss trotzdem vorsichtig sein (Stichworte aggressive optimization, full-program (link-time) optimization etc.)
 
Zuletzt bearbeitet:
Dabei seit
Dez. 2011
Beiträge
203
#8
dann hat man allerdings (nIter - 1) mal eventuelle verfälschungen durch caching-effekte.
Caching-effekte koennen dich immer treffen, allein schon wenn das OS deiner Applikation den Ausfuehrungsthread wegnimmt. Und beim profilen von Codeabschnitten, welche u.U. in multiplen Kontexten aufgerufen werden (und hier caching etc. dann eine Rolle spielen mag) kann man ohne besagten Kontext sowieso nicht 100% exakt profilen, weil solche Abschnitte in real halt mehr als nur eine Exekutionsdauer haben.

Im Umkehrschluss heisst das natuerlich auch, dass man obigen Code ja an den Kontext anpassen kann. Fuerchtet man etwa eine nennenswerte Auswirkung von Cache-effekten so braucht man den Beginn von callFunc ja nur entsprechend mit irgendeinem cache-intensiven Muell aufzufuellen. Will man das nur manchmal haben, kann man diesen Muell nur bei jeder x-ten Iteration durchlaufen lassen. Wird der gewuenschte Abschnitt im Realcode eh in einer Schleife ausgefuehrt so macht der obige Code wiederum genau das richtige.

Neben cache-misses koennen auch branch-prediction issues eine Rolle spielen => wieder gilt die Schleife bzw. callFunc entsprechend modifizeren dass es dem realen Kontext nahe hinkommt.

Sieh den demonstrieten Code mehr als ein Grundgeruest an. Aber in den meisten Faellen wird die Basisversion schon vollkommen ausreichen :)
 

slashmaxx

Cadet 3rd Year
Dabei seit
Okt. 2006
Beiträge
47
#9
old-school ist gut :).

Es löst aber nicht das Problem der sehr groben Zeitauflösung, welche ev. >> all deine Codezeilen zusammen ist.

Ich kenne diesen +- plattformunabhängigen old-school Trick:

Code:
int nIter = 1000000; // auf einen für das Problem sinnvollen Wert setzen

startTimeMeasurment();

for (int i = 0; i < nIter;++i)
  callFunc();

stopTimeMeasurment();
und callFunc schaut einmal so aus:

Code:
int callFunc(int arg1, double arg2)
{ return 0; }
und einmal so:

Code:
int callFunc(int arg1, double arg2)
{
  // code to measure goes in here

return 0;
}

Man lässt das nun für beide Varianten von callFunc durchlaufen und misst jeweils die Zeit -> die Nettozeit des zu messenden Codes sollte sich dann als Differenz der Durchläufe ergeben. Natürlich sollte nIter so gewählt werden, dass die gesamte Laufzeit >> der Zeitauflösung der Maschine ist.
Der Vorteil ist, dass aller overhead, von Zeitmessung über looping bis function call overheads (incl. parameter-passing), exakt rausgerechnet werden müsste.

Das Problem in der Praxis ist den Compiler so auszutricksen, dass der den Aufruf der nichtstuenden callFunc (oder auch der anderen, je nachdem was die berechnet) nicht als unnötigen Funktionsaufruf erkennt und somit (ev. samt ganzer Schleife) schlicht wegoptimiert (= nicht exekutiert). In C++ hilft wenn callFunc eine virtuelle Funktion ist, oder in C/C++ wenn was in eine volatile Variable reingeschrieben wird, aber man muss trotzdem vorsichtig sein (Stichworte aggressive optimization, full-program (link-time) optimization etc.)

Eine internen Zeitmessungen ist natürlich genauso möglich, aber hat offentsichtlich den Nachteil, dass diese höchsten mit der Auflösung der Taktung stattfinden kann. Natürlich gilt auch bei externer Zeitmessung, dass die Taktung des externen Systems die Auflösung bestimmt, aber die ist bei geeignetem Oszilloskop um ein vielfaches höher (man muss natürlich erstmal sowas haben). Sogesehen ist deine Variante "günstiger" in der Anschaffung :-). Je nach dem wie die interne Zeitmessung nun abläuft, kann diese aber ebenfalls auf die Codeausführung einen Einfluss haben (z.B: Zeitzählung durch Interrupts).

Die Iteration ist mit beiden Varianten (intern und extern) möglich und erlaubt eine statistisch aussagekräftige Abschätzung über die Laufzeit, die nicht-periodische bzw. sporadisch auftretende Interrupts und somit den Jitter mittelt und ausgleicht. Periodisch auftretende Effekte (z.B. durch Zeitmessinterrupts oder Cachings) bleiben aber voll drin. Somit gibt diese Methode eine Einblick in die Brutto-Perfomance, der zu messenden Funktion.

Und immer gilt für alles: Wer viel misst, misst Mist. :-)

Gruß slash
 
Zuletzt bearbeitet:
Dabei seit
Dez. 2011
Beiträge
203
#10
bei geeignetem Oszilloskop um ein vielfaches höher (man muss natürlich erstmal sowas haben).
Welche Auflösung erreichen die eigentlich und von welcher Preisspanne reden wir da? Ich wüsste übrigens nicht mal eine Stelle wo sowas verwendet wird oder mal getestet werden kann - geschweige schon wie ich sowas an meinem Arbeitslaptop anschliessen sollte ;).

Je nach dem wie die interne Zeitmessung nun abläuft, kann diese aber ebenfalls auf die Codeausführung einen Einfluss haben (z.B: Zeitzählung durch Interrupts).
Nicht nur die interne, auch deine externe -> SetIO() und ClearIO() sind ja nicht gratis. Irgendeinen overhead hat man immer, aber wenn die Zeitmessfunktionenper se außerhalb der Schleife sind wird ihr Einfluss auf Codebene minimiert. Ob das System dann zusätzlich interrupts oder anderes macht, was ohne der Zeitmessung nicht ausgeführt worden wäre, hängt natürlich vom System ab.
Irgendwie hats philosophisch was von Quantenphysik: Man kann eigentlich nicht beobachten, ohne das zu beobachtende dadurch zu beeinflussen ;).

Ansonsten ist es wie du sagst, man misst eine Bruttoperformance und sollte es mit messen nicht übertreiben. Keine zu kurzen Codeabschnitte messen, Ergebnisse v.a. relativ sehen, und eine grobe Ahnung vom Messunsicherheitsbereich haben :)
 

slashmaxx

Cadet 3rd Year
Dabei seit
Okt. 2006
Beiträge
47
#11
Die günstigen Oszis fangen so bei 100MSamples/s und gehen z.B. bis 20GSamples/s oder mehr. Bei 10ms (Grundtakt?) ist das sicher mehr.
Da es hier ein FPGA ist, ging ich natürlich naiverweise von einem FPGA Entwicklungsboard aus, wo man in der Regel entsprechen Pins hat, wo man das anschließt. Dann einfach mal in Bedienungshandbuch des Oszis gucken :-), viele haben auch eine passende PC Schnittstelle (USB oder so).

Na klar sind die SetIO() und ClearIO() Funktionen nicht gratis, die verkaufe ich für 10 Cent das Stk ;-) Nee Scherz beiseite. Der Unterschied ist, dass diese Funktionen rein singuläre Funktionen sind, die durch eine Differenzmessung herausgerechnet werden können. Während der Messung selbst treten keine durch die Messmethode selbst verursachten Verzögerungen auf.

Je nachdem was die startTimeMeasurment()-Funktion tut kann diese:
a) Singulär einen Hardware-Timer starten, stoppen und danach auslesen (beste Variante und entspricht der IO Methode)
b) Einen Software-Timer starten, welche per Interrupts die Zeit zählt (kann ungünstig sein, da hier die Interrupts die Ausführung kurzeitig blockieren und somit die zu messende Funktion beeinflusst)

Ich verwende selber beide Messmethoden nach Bedarf gemischt.

Irgendwie hats philosophisch was von Quantenphysik: Man kann eigentlich nicht beobachten, ohne das zu beobachtende dadurch zu beeinflussen .

Ansonsten ist es wie du sagst, man misst eine Bruttoperformance und sollte es mit messen nicht übertreiben. Keine zu kurzen Codeabschnitte messen, Ergebnisse v.a. relativ sehen, und eine grobe Ahnung vom Messunsicherheitsbereich haben
Stimme dir voll zu ;-)
 
Top