Der beste Weg, um die Code-Geschwindigkeit in C ++ ohne Profiler zu testen, oder macht es keinen Sinn zu versuchen?

8

Bei SO gibt es eine ganze Reihe von Fragen zum Leistungsprofil, aber ich finde nicht das ganze Bild. Es gibt einige Probleme und die meisten Q & amp; A ignorieren alle außer ein paar auf einmal oder rechtfertigen ihre Vorschläge nicht.

Worüber ich mich wundere. Wenn ich zwei Funktionen habe, die dasselbe tun, und bin ich neugierig auf den Unterschied in der Geschwindigkeit, macht es Sinn, dies ohne externe Tools, mit Timern zu testen, oder wird dies im Test die Ergebnisse zu viel beeinflussen?

Ich frage das, denn wenn es sinnvoll ist, als C ++ - Programmierer, möchte ich wissen, wie es am besten geht, da sie viel einfacher sind als externe Tools. Wenn es sinnvoll ist, lasst uns mit allen möglichen Fallen fortfahren:

Betrachten Sie dieses Beispiel. Der folgende Code zeigt 2 Möglichkeiten, das Gleiche zu tun:

%Vor%

Die Probleme:

  1. Welche Timer verwenden Sie und wie wird die CPU-Zeit tatsächlich vom betreffenden Code verbraucht?
  2. Was sind die Auswirkungen der Compiler-Optimierung (da diese Funktionen nur Bytes hin- und herwechseln, ist es am effizientesten, überhaupt nichts zu tun)?
  3. Glauben Sie angesichts der hier vorgestellten Ergebnisse, dass sie genau sind (ich kann Ihnen versichern, dass mehrere Läufe sehr ähnliche Ergebnisse liefern)? Wenn ja, können Sie erklären, wie std :: reverse in Anbetracht der Einfachheit der benutzerdefinierten Funktion so schnell sein wird. Ich habe nicht den Quellcode aus der Vc ++ Version, die ich für diesen Test verwendet habe, aber hier ist die Implementierung von GNU. Es läuft auf die Funktion iter_swap hinaus, was völlig unverständlich ist für mich. Würde das auch doppelt so schnell laufen wie diese benutzerdefinierte Funktion, und wenn ja, warum?

Betrachtungen:

  1. Zwei hochpräzise Timer werden vorgeschlagen: clock () und QueryPerformanceCounter (unter Windows). Offensichtlich möchten wir die CPU-Zeit unseres Codes und nicht die Echtzeit messen, aber soweit ich weiß, geben diese Funktionen diese Funktionalität nicht, so dass andere Prozesse im System die Messungen stören würden. Diese Seite auf der GNU-C-Bibliothek scheint dem zu widersprechen, aber wenn ich in vc ++ einen Breakpoint setze, bekommt der debugged-Prozess eine Menge Clock-Ticks, obwohl er gesperrt war (ich habe nicht unter gnu getestet). Fehle ich dafür alternative Zähler oder brauchen wir dafür mindestens spezielle Bibliotheken oder Klassen? Wenn nicht, ist die Uhr in diesem Beispiel gut genug oder gibt es einen Grund, den QueryPerformanceCounter zu verwenden?

  2. Was können wir ohne Debugging-, Disassemblier- und Profiling-Tools sicher wissen? Passiert tatsächlich etwas? Ist der Funktionsaufruf inline oder nicht? Beim Einchecken des Debuggers werden die Bytes tatsächlich ausgetauscht, aber ich würde lieber aus der Theorie wissen, als aus dem Testen.

Danke für jede Wegbeschreibung.

update

Danke an Hinweis von tojas Die Funktion swapBytes läuft jetzt genauso schnell als die std :: reverse. Ich hatte nicht erkannt, dass die temporäre Kopie im Falle eines Bytes nur ein Register sein muss und somit sehr schnell ist. Eleganz kann dich blenden.

%Vor%

Danke an Tipp von ChrisW Ich habe das unter Windows gefunden kann die tatsächliche CPU-Zeit, die von einem (lesen: Ihr) Prozess durch Windows-Verwaltung verbraucht wird, erhalten Instrumentierung Das sieht definitiv interessanter aus als der Hochpräzisionszähler.

    
nus 27.06.2010, 17:01
quelle

8 Antworten

4
  

Offensichtlich möchten wir die CPU-Zeit unseres Codes und nicht die Echtzeit messen, aber soweit ich verstehe, geben diese Funktionen diese Funktionalität nicht, so dass andere Prozesse im System die Messungen stören würden.

Ich mache zwei Dinge, um sicherzustellen, dass die Uhrzeit der Wanduhr und die CPU-Zeit ungefähr gleich sind:

  • Test für eine signifikante Zeitspanne, d. h. mehrere Sekunden (z. B. durch Testen einer Schleife von mehreren tausend Iterationen)

  • Testen Sie, ob die Maschine mehr oder weniger im Leerlauf ist, außer für alles, was ich gerade teste.

Alternativ, wenn Sie nur die CPU-Zeit pro Thread genauer messen möchten, steht diese als Leistungsindikator zur Verfügung (siehe z. B. perfmon.exe ).

  

Was können wir ohne Debugging-, Disassemblier- und Profiling-Tools sicher wissen?

Fast nichts (außer dass I / O relativ langsam ist).

    
ChrisW 27.06.2010, 17:37
quelle
2

Um Ihre Hauptfrage zu beantworten, vertauscht der "umgekehrte" Algorithmus nur Elemente aus dem Array und arbeitet nicht mit den Elementen des Arrays.

    
tojas 27.06.2010 17:49
quelle
2

Verwenden Sie QueryPerformanceCounter unter Windows, wenn Sie ein Timing mit hoher Auflösung benötigen. Die Zählergenauigkeit hängt von der CPU ab, kann aber pro Takt erreicht werden. Profiling im realen Betrieb ist jedoch immer eine bessere Idee.

    
Puppy 27.06.2010 18:06
quelle
2

Ist es sicher zu sagen, dass Sie zwei Fragen stellen?

  • Welcher ist schneller und um wieviel?

  • Und warum ist es schneller?

Für den ersten brauchen Sie keine hochpräzisen Timer. Alles, was Sie tun müssen, ist sie "lang genug" laufen zu lassen und mit Zeitgebern mit geringer Präzision zu messen. (Ich bin altmodisch, meine Armbanduhr hat eine Stoppuhr-Funktion, und es ist völlig gut genug.)

Zweitens können Sie den Code sicher unter einem Debugger ausführen und ihn auf Anweisungsebene in einem Schritt ausführen. Da die grundlegenden Operationen so einfach sind, können Sie grob sehen, wie viele Anweisungen für den grundlegenden Zyklus erforderlich sind.

Denk einfach. Leistung ist kein schweres Thema. Normalerweise versuchen Leute Probleme zu finden , für die Dies ist ein einfacher Ansatz .

    
Mike Dunlavey 27.06.2010 18:02
quelle
2

(Diese Antwort bezieht sich nur auf Windows XP und den 32-Bit-VC ++ - Compiler.)

Am einfachsten ist es, wenn Sie den Code des Zeitstempels der CPU ein wenig timestimulieren. Dies ist ein 64-Bit-Wert, eine Zählung der Anzahl der bisher ausgeführten CPU-Zyklen, was eine ungefähr so ​​gute Auflösung ist, wie Sie bekommen werden. Die tatsächlichen Zahlen, die Sie erhalten, sind nicht besonders nützlich, da sie stehen, aber wenn Sie mehrere Läufe verschiedener konkurrierender Ansätze ausmitteln, können Sie sie auf diese Weise vergleichen. Die Ergebnisse sind ein wenig laut, aber immer noch zu Vergleichszwecken gültig.

Um den Zeitstempel-Zähler zu lesen, verwenden Sie einen Code wie den folgenden:

%Vor%

(Mit der Anweisung cpuid wird sichergestellt, dass keine unvollständigen Anweisungen auf die Ausführung warten.)

Es gibt vier Dinge, die bei diesem Ansatz wert sind.

Erstens wird es wegen der Inline-Assembler-Sprache auf dem x64-Compiler von MS nicht so funktionieren wie es ist. (Sie müssen eine .ASM-Datei mit einer Funktion darin erstellen. Eine Übung für den Leser; ich kenne die Details nicht.)

Zweitens, um Probleme mit Zykluszählern zu vermeiden, die nicht über verschiedene Kerne / Threads / was Sie haben, synchronisieren, müssen Sie möglicherweise die Affinität Ihres Prozesses so festlegen, dass er nur auf einer bestimmten Ausführungseinheit ausgeführt wird. (Dann wieder ... Sie dürfen nicht.)

Drittens möchten Sie auf jeden Fall die generierte Assemblersprache überprüfen, um sicherzustellen, dass der Compiler ungefähr den Code generiert, den Sie erwarten. Achten Sie darauf, dass Teile des Codes entfernt werden, Funktionen inline sind, so etwas.

Schließlich sind die Ergebnisse ziemlich laut. Die Zykluszähler zählen Zyklen, die für alles ausgegeben werden, einschließlich Warten auf Caches, Zeitaufwand für die Ausführung anderer Prozesse, Zeitaufwand im Betriebssystem selbst usw. Leider ist es (zumindest unter Windows) nicht möglich, nur Ihren Prozess zu synchronisieren. Also, ich schlage vor, den getesteten Code oft (einige Zehntausende) zu testen und den Durchschnitt zu berechnen. Das ist nicht sehr listig, aber es scheint auf jeden Fall nützliche Ergebnisse für mich hervorgebracht zu haben.

    
please delete me 27.06.2010 20:03
quelle
1

Ich nehme an, dass jemand, der kompetent genug ist, alle Ihre Fragen zu beantworten, viel zu beschäftigt ist, um alle Ihre Fragen zu beantworten. In der Praxis ist es wahrscheinlich effektiver, einzelne, wohldefinierte Fragen zu stellen. Auf diese Weise können Sie hoffen, klar definierte Antworten zu erhalten, die Sie sammeln können und auf Ihrem Weg zur Weisheit sind.

Wie auch immer, vielleicht kann ich Ihre Frage über die Uhr unter Windows beantworten.

clock () wird nicht als Hochpräzisionstakt betrachtet. Wenn Sie den Wert von CLOCKS_PER_SEC betrachten, sehen Sie, dass er eine Auflösung von 1 Millisekunde hat. Dies ist nur angemessen, wenn Sie sehr lange Routinen oder eine Schleife mit 10000 Iterationen einteilen. Wie Sie darauf hinweisen, wenn Sie versuchen, eine einfache Methode 10000 von Zeiten zu wiederholen, um eine Zeit zu erhalten, die mit clock () gemessen werden kann, wird der Compiler wahrscheinlich einspringen und die ganze Sache weg optimieren.

Die einzige zu verwendende Uhr ist also QueryPerformanceCounter ()

    
ravenspoint 27.06.2010 17:24
quelle
1

Gibt es etwas, das Sie gegen Profiler haben? Sie helfen eine Tonne. Da Sie auf WinXP sind, sollten Sie eine Probeversion von vtune ausprobieren. Versuchen Sie einen Call Graph Sampling Test und schauen Sie sich Eigenzeit und Gesamtzeit der aufgerufenen Funktionen an. Es gibt keinen besseren Weg, Ihr Programm so zu optimieren, dass es schnellstmöglich ist, ohne ein Assemblergenie zu sein (und ein wirklich außergewöhnliches).

Manche Leute scheinen nur auf Profiler allergisch zu sein. Ich war einer von denen und dachte, ich wüsste am besten, wo meine Hotspots waren. Ich hatte oft recht, wenn es um offensichtliche algorithmische Ineffizienzen ging, aber bei immer mehr Mikrooptimierungsfällen praktisch immer falsch. Das einfache Umschreiben einer Funktion ohne Änderung der Logik (z. B. Neuordnung von Dingen, Einfügen von Sonderfallcode in eine separate, nicht inlinierte Funktion usw.) kann Funktionen ein Dutzend Mal schneller machen, und selbst die besten Disassemblierungs-Experten können dies normalerweise nicht vorhersagen ohne den Profiler.

Da sie sich auf simple Timing-Tests verlassen, sind sie extrem problematisch. Dieser aktuelle Test ist nicht so schlecht, aber es ist ein sehr verbreiteter Fehler, Timing-Tests auf eine Art und Weise zu schreiben, bei der der Optimierer den toten Code optimiert und am Ende die Zeit testet, die benötigt wird, um im Wesentlichen einen NOP oder sogar gar nichts zu machen. Sie sollten etwas wissen, um die Disassemblierung zu interpretieren, um sicherzustellen, dass der Compiler dies nicht tut.

Auch Timing-Tests wie diese tendieren dazu, die Ergebnisse signifikant zu verzerren, da viele von ihnen nur den Code in der gleichen Schleife durchlaufen lassen, was dazu tendiert, einfach den Effekt Ihres Codes zu testen, wenn der gesamte Speicher in der Cache mit all den Verzweigungsvorhersage funktioniert perfekt für sie. Es zeigt oft nur Best-Case-Szenarien, ohne Ihnen den durchschnittlichen, realen Fall zu zeigen.

Abhängig von realen Zeitmessungstests ist es ein bisschen besser; etwas näher an dem, was Ihre Anwendung auf hohem Niveau tun wird. Es wird Ihnen keine Details darüber geben, wie viel Zeit in Anspruch genommen wird, aber genau das soll der Profiler tun.

    
stinky472 27.06.2010 17:24
quelle
-3

Was? Wie misst man die Geschwindigkeit ohne einen Profiler? Genau das Messen von Geschwindigkeit ist Profiling! Die Frage lautet: "Wie kann ich meinen eigenen Profiler schreiben?" Und die Antwort ist eindeutig, "nicht".

Außerdem solltest du std::swap an erster Stelle verwenden, wodurch die sinnlose Verfolgung komplett zunichte gemacht wird.

-1 für Sinnlosigkeit.

    
John 27.06.2010 19:18
quelle

Tags und Links