Wie kann ich C ++ - Code unter Linux profilieren?


1816

Ich habe eine C ++ - Anwendung unter Linux, die ich gerade optimiere. Wie kann ich feststellen, welche Bereiche meines Codes langsam ausgeführt werden?


27
Wenn Sie mehr Daten zu Ihrem Entwicklungsstapel bereitstellen, erhalten Sie möglicherweise bessere Antworten. Es gibt Profiler von Intel und Sun, aber Sie müssen deren Compiler verwenden. Ist das eine Option?
Nazgob

2
Es wird bereits auf dem folgenden Link beantwortet: stackoverflow.com/questions/2497211/…
Kapil Gupta

4
Die meisten Antworten sind codeProfiler. Prioritätsumkehr, Cache-Aliasing, Ressourcenkonflikte usw. können jedoch Faktoren für die Optimierung und Leistung sein. Ich denke, dass Leute Informationen in meinen langsamen Code lesen . FAQs verweisen auf diesen Thread.
Kunstloser Lärm


3
Früher habe ich pstack zufällig verwendet. Meistens wird der typischste Stack ausgedruckt, in dem sich das Programm die meiste Zeit befindet, was auf den Engpass hinweist.
Jose Manuel Gomez Alvarez

Antworten:


1406

Wenn Sie einen Profiler verwenden möchten, verwenden Sie einen der vorgeschlagenen.

Wenn Sie es jedoch eilig haben und Ihr Programm unter dem Debugger manuell unterbrechen können, während es subjektiv langsam ist, gibt es eine einfache Möglichkeit, Leistungsprobleme zu finden.

Halten Sie es einfach mehrmals an und sehen Sie sich jedes Mal den Aufrufstapel an. Wenn es einen Code gibt, der einen bestimmten Prozentsatz der Zeit verschwendet, 20% oder 50% oder was auch immer, ist dies die Wahrscheinlichkeit, dass Sie ihn bei jeder Stichprobe auf frischer Tat ertappen. Das ist also ungefähr der Prozentsatz der Proben, auf denen Sie es sehen werden. Es sind keine fundierten Vermutungen erforderlich. Wenn Sie eine Vermutung haben, was das Problem ist, wird dies es beweisen oder widerlegen.

Möglicherweise haben Sie mehrere Leistungsprobleme unterschiedlicher Größe. Wenn Sie einen von ihnen entfernen, nehmen die verbleibenden einen größeren Prozentsatz ein und sind bei nachfolgenden Durchgängen leichter zu erkennen. Dieser Vergrößerungseffekt kann , wenn er über mehrere Probleme hinweg verstärkt wird, zu wirklich massiven Beschleunigungsfaktoren führen.

Vorsichtsmaßnahme : Programmierer stehen dieser Technik eher skeptisch gegenüber, es sei denn, sie haben sie selbst verwendet. Sie werden sagen, dass Profiler Ihnen diese Informationen geben, aber das ist nur wahr, wenn sie den gesamten Aufrufstapel abtasten und Sie dann einen zufälligen Satz von Stichproben untersuchen lassen. (In den Zusammenfassungen geht die Einsicht verloren.) Anrufdiagramme geben Ihnen nicht die gleichen Informationen, weil

  1. Sie fassen nicht auf Anweisungsebene zusammen, und
  2. Sie geben verwirrende Zusammenfassungen bei Rekursion.

Sie werden auch sagen, dass es nur bei Spielzeugprogrammen funktioniert, wenn es tatsächlich bei jedem Programm funktioniert, und es scheint bei größeren Programmen besser zu funktionieren, da sie tendenziell mehr Probleme zu finden haben. Sie werden sagen, dass es manchmal Dinge findet, die keine Probleme sind, aber das ist nur wahr, wenn Sie etwas einmal sehen . Wenn Sie ein Problem bei mehr als einer Probe sehen, ist es real.

PS Dies kann auch in Multithread-Programmen durchgeführt werden, wenn es eine Möglichkeit gibt, Call-Stack-Beispiele des Thread-Pools zu einem bestimmten Zeitpunkt zu erfassen, wie dies in Java der Fall ist.

PPS Im Allgemeinen gilt: Je mehr Abstraktionsebenen in Ihrer Software vorhanden sind, desto wahrscheinlicher ist es, dass dies die Ursache für Leistungsprobleme ist (und die Möglichkeit, sich zu beschleunigen).

Hinzugefügt : Es ist möglicherweise nicht offensichtlich, aber die Stapelabtasttechnik funktioniert bei Rekursion gleich gut. Der Grund dafür ist, dass die Zeit, die durch Entfernen eines Befehls eingespart werden würde, durch den Anteil der Proben, die ihn enthalten, angenähert wird, unabhängig davon, wie oft er innerhalb einer Probe auftreten kann.

Ein weiterer Einwand, den ich oft höre, ist: " Es wird irgendwo zufällig aufhören und das eigentliche Problem übersehen. " Dies ergibt sich aus einem vorherigen Konzept des eigentlichen Problems. Eine wichtige Eigenschaft von Leistungsproblemen ist, dass sie den Erwartungen trotzen. Die Probenahme sagt Ihnen, dass etwas ein Problem ist, und Ihre erste Reaktion ist Unglaube. Das ist natürlich, aber Sie können sicher sein, dass ein Problem real ist und umgekehrt.

Hinzugefügt : Lassen Sie mich eine Bayes'sche Erklärung geben, wie es funktioniert. Angenommen, es gibt eine Anweisung I(Aufruf oder auf andere Weise), die sich zu einem Bruchteil fder Zeit auf dem Aufrufstapel befindet (und daher so viel kostet). Nehmen wir zur Vereinfachung an, wir wissen nicht, was fist, aber nehmen wir an , dass es entweder 0,1, 0,2, 0,3, ... 0,9, 1,0 ist und die vorherige Wahrscheinlichkeit für jede dieser Möglichkeiten 0,1 beträgt, sodass alle diese Kosten gleich wahrscheinlich sind a-priori.

Nehmen wir dann an, wir nehmen nur 2 Stapelproben und sehen Anweisungen Izu beiden Proben, die als Beobachtung bezeichnet werden o=2/2. Dies gibt uns neue Schätzungen der Häufigkeit fvon I:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&&f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.1    1     1             0.1          0.1            0.25974026
0.1    0.9   0.81          0.081        0.181          0.47012987
0.1    0.8   0.64          0.064        0.245          0.636363636
0.1    0.7   0.49          0.049        0.294          0.763636364
0.1    0.6   0.36          0.036        0.33           0.857142857
0.1    0.5   0.25          0.025        0.355          0.922077922
0.1    0.4   0.16          0.016        0.371          0.963636364
0.1    0.3   0.09          0.009        0.38           0.987012987
0.1    0.2   0.04          0.004        0.384          0.997402597
0.1    0.1   0.01          0.001        0.385          1

                  P(o=2/2) 0.385                

In der letzten Spalte heißt es, dass beispielsweise die Wahrscheinlichkeit, dass f> = 0,5 ist, 92% beträgt, verglichen mit der vorherigen Annahme von 60%.

Angenommen, die vorherigen Annahmen sind unterschiedlich. Angenommen, wir nehmen an, dass P(f=0.1)0,991 (fast sicher) ist und alle anderen Möglichkeiten fast unmöglich sind (0,001). Mit anderen Worten, unsere vorherige Gewissheit ist, dass dies Ibillig ist. Dann bekommen wir:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&& f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.001  1    1              0.001        0.001          0.072727273
0.001  0.9  0.81           0.00081      0.00181        0.131636364
0.001  0.8  0.64           0.00064      0.00245        0.178181818
0.001  0.7  0.49           0.00049      0.00294        0.213818182
0.001  0.6  0.36           0.00036      0.0033         0.24
0.001  0.5  0.25           0.00025      0.00355        0.258181818
0.001  0.4  0.16           0.00016      0.00371        0.269818182
0.001  0.3  0.09           0.00009      0.0038         0.276363636
0.001  0.2  0.04           0.00004      0.00384        0.279272727
0.991  0.1  0.01           0.00991      0.01375        1

                  P(o=2/2) 0.01375                

Jetzt heißt P(f >= 0.5)es 26%, gegenüber der vorherigen Annahme von 0,6%. Bayes ermöglicht es uns daher, unsere Schätzung der wahrscheinlichen Kosten von zu aktualisieren I. Wenn die Datenmenge gering ist, können wir nicht genau sagen, wie hoch die Kosten sind, sondern nur, dass sie groß genug sind, um repariert zu werden.

Eine weitere Sichtweise ist die Nachfolge-Regel . Wenn Sie eine Münze zweimal werfen und beide Male auftauchen, was sagt Ihnen das über die wahrscheinliche Gewichtung der Münze? Die respektierte Art zu antworten ist zu sagen, dass es sich um eine Beta-Distribution mit durchschnittlichem Wert handelt (number of hits + 1) / (number of tries + 2) = (2+1)/(2+2) = 75%.

(Der Schlüssel ist, dass wir Imehr als einmal sehen. Wenn wir es nur einmal sehen, sagt uns das nicht viel, außer dass f> 0.)

Selbst eine sehr kleine Anzahl von Mustern kann uns viel über die Kosten von Anweisungen erzählen, die sie sehen. (Und es wird sie mit einer Frequenz im Durchschnitt proportional zu ihren Kosten sehen. Wenn nProben genommen werden, und fdie Kosten, dann Iauf erscheinen nf+/-sqrt(nf(1-f))Proben. Beispiel, n=10, f=0.3, dh 3+/-1.4Proben.)


Hinzugefügt : Um ein intuitives Gefühl für den Unterschied zwischen Mess- und Zufallsstapelstichproben zu vermitteln:
Es gibt jetzt Profiler, die den Stapel auch zur Wanduhrzeit abtasten. Was jedoch herauskommt, sind Messungen (oder Hot Path oder Hot Spot, von denen aus ein "Engpass" kann sich leicht verbergen). Was sie Ihnen nicht zeigen (und sie könnten es leicht), sind die tatsächlichen Proben selbst. Und wenn Ihr Ziel darin besteht, den Engpass zu finden , müssen Sie im Durchschnitt 2 sehen, geteilt durch den Zeitanteil. Wenn es also 30% der Zeit dauert, zeigen 2 / .3 = 6,7 Proben im Durchschnitt dies, und die Wahrscheinlichkeit, dass 20 Proben es zeigen, beträgt 99,2%.

Hier ist eine direkte Darstellung des Unterschieds zwischen der Untersuchung von Messungen und der Untersuchung von Stapelproben. Der Engpass könnte ein großer Blob wie dieser oder zahlreiche kleine sein, es macht keinen Unterschied.

Geben Sie hier die Bildbeschreibung ein

Die Messung erfolgt horizontal; Hier erfahren Sie, wie viel Zeit bestimmte Routinen in Anspruch nehmen. Die Probenahme erfolgt vertikal. Wenn es eine Möglichkeit gibt, zu vermeiden, was das gesamte Programm in diesem Moment tut, und wenn Sie es in einem zweiten Beispiel sehen , haben Sie den Engpass gefunden. Das ist es, was den Unterschied ausmacht - den ganzen Grund für die aufgewendete Zeit zu sehen, nicht nur wie viel.


292
Dies ist im Grunde ein Stichprobenprofiler für arme Männer, was großartig ist, aber Sie laufen Gefahr, dass die Stichprobengröße zu klein ist, was möglicherweise zu völlig falschen Ergebnissen führt.
Crashworks

100
@Crash: Ich werde nicht über den Teil "armer Mann" diskutieren :-) Es ist wahr, dass statistische Messgenauigkeit viele Stichproben erfordert, aber es gibt zwei widersprüchliche Ziele - Messung und Problemlokalisierung. Ich konzentriere mich auf Letzteres, für das Sie eine genaue Position und keine genaue Genauigkeit benötigen. So kann es beispielsweise im Mid-Stack einen einzelnen Funktionsaufruf A () geben; das macht 50% der Zeit aus, aber es kann in einer anderen großen Funktion B sein, zusammen mit vielen anderen Aufrufen von A (), die nicht teuer sind. Genaue Zusammenfassungen der Funktionszeiten können ein Hinweis sein, aber jedes zweite Stapelmuster wird das Problem genau bestimmen.
Mike Dunlavey

41
... die Welt scheint zu denken, dass ein Anrufdiagramm, das mit Anrufzahlen und / oder durchschnittlichem Timing versehen ist, gut genug ist. Es ist nicht. Und der traurige Teil ist, dass für diejenigen, die den Aufrufstapel abtasten, die nützlichsten Informationen direkt vor ihnen liegen, aber sie werfen sie im Interesse der "Statistik" weg.
Mike Dunlavey

30
Ich will Ihrer Technik nicht widersprechen. Natürlich verlasse ich mich ziemlich stark auf Stack-Walking-Sampling-Profiler. Ich möchte nur darauf hinweisen, dass es einige Tools gibt, die dies jetzt automatisiert tun. Dies ist wichtig, wenn Sie eine Funktion von 25% auf 15% überschritten haben und sie von 1,2% auf 1,2% reduzieren müssen 0,6%.
Crashworks

13
-1: Gute Idee, aber wenn Sie dafür bezahlt werden, in einer mäßig leistungsorientierten Umgebung zu arbeiten, ist dies Zeitverschwendung für alle. Verwenden Sie einen echten Profiler, damit wir nicht hinter Ihnen herkommen und die eigentlichen Probleme beheben müssen.
Sam Harwell

583

Sie können Valgrind mit den folgenden Optionen verwenden

valgrind --tool=callgrind ./(Your binary)

Es wird eine Datei mit dem Namen generiert callgrind.out.x. Sie können kcachegrinddiese Datei dann mit dem Tool lesen. Sie erhalten eine grafische Analyse der Dinge mit Ergebnissen, z. B. welche Linien wie viel kosten.


51
Valgrind ist großartig, aber seien Sie gewarnt, dass es Ihr Programm verdammt langsam machen wird
neves

30
Schauen Sie sich auch Gprof2Dot an, um eine erstaunliche Alternative zur Visualisierung der Ausgabe zu finden. ./gprof2dot.py -f callgrind callgrind.out.x | dot -Tsvg -o output.svg
Sebastian

2
@neves Ja Valgrind ist in Bezug auf die Geschwindigkeit für die Echtzeit-Profilerstellung von "gstreamer" - und "opencv" -Anwendungen einfach nicht sehr hilfreich.
Enthusiastgeek

1
stackoverflow.com/questions/375913/… ist eine Teillösung für Geschwindigkeitsprobleme.
Tõnu Samuel

3
@ Sebastian: gprof2dotist jetzt hier: github.com/jrfonseca/gprof2dot
John Zwinck

348

Ich nehme an, Sie verwenden GCC. Die Standardlösung wäre das Profilieren mit gprof .

Stellen Sie sicher, dass Sie -pgzur Kompilierung hinzufügen , bevor Sie ein Profil erstellen:

cc -o myprog myprog.c utils.c -g -pg

Ich habe es noch nicht ausprobiert, aber ich habe gute Dinge über Google-Perftools gehört . Es ist definitiv einen Versuch wert.

Verwandte Frage hier .

Ein paar andere Schlagworte, wenn gprofsie den Job nicht für Sie erledigen: Valgrind , Intel VTune , Sun DTrace .


3
Ich stimme zu, dass gprof der aktuelle Standard ist. Nur eine Anmerkung: Valgrind wird verwendet, um Speicherlecks und andere speicherbezogene Aspekte Ihrer Programme zu profilieren, nicht zur Geschwindigkeitsoptimierung.
Bill the Lizard

68
Bill, In der Vaglrind Suite finden Sie Callgrind und Massiv. Beide sind ziemlich nützlich, um Apps zu profilieren
dario minonne

7
@ Bill-the-Lizard: Einige Kommentare zu gprof : stackoverflow.com/questions/1777556/alternatives-to-gprof/…
Mike Dunlavey

6
gprof -pg ist nur eine Annäherung an die Callstack-Profilerstellung. Es fügt mcount-Aufrufe ein, um zu verfolgen, welche Funktionen welche anderen Funktionen aufrufen. Es verwendet eine zeitbasierte Standardabtastung für die Zeit. Anschließend werden die in einer Funktion foo () abgetasteten Zeiten entsprechend der Anzahl der Aufrufe an die Aufrufer von foo () zurückgegeben. Es wird also nicht zwischen Anrufen mit unterschiedlichen Kosten unterschieden.
Krazy Glew

1
Mit clang / clang ++ könnte man in Betracht ziehen , den CPU-Profiler von gperftools zu verwenden . Vorsichtsmaßnahme: Habe ich selbst nicht getan.
Einpoklum

257

Neuere Kernel (z. B. die neuesten Ubuntu-Kernel) werden mit den neuen 'perf'-Tools ( apt-get install linux-tools) AKA perf_events geliefert .

Diese werden mit klassischen Sampling-Profilern ( Manpage ) sowie dem fantastischen Zeitdiagramm geliefert !

Wichtig ist, dass diese Tools Systemprofile und nicht nur Prozessprofile erstellen können. Sie können die Interaktion zwischen Threads, Prozessen und dem Kernel darstellen und Sie über die Planung und die E / A-Abhängigkeiten zwischen Prozessen informieren.

Alt-Text


12
Tolles Werkzeug! Gibt es für mich überhaupt eine typische "Schmetterlings" -Ansicht, die vom "main-> func1-> fun2" -Stil ausgeht? Ich kann das nicht herausfinden ... perf reportscheint mir die Funktionsnamen mit den
anrufenden

Will kann das Zeitdiagramm der Thread-Aktivität anzeigen. mit Informationen zur CPU-Nummer hinzugefügt? Ich möchte sehen, wann und welcher Thread auf jeder CPU ausgeführt wurde.
Osgx

2
@ kizzx2 - du kannst gprof2dotund verwenden perf script. Sehr schönes Werkzeug!
Bindestrich

2
Selbst neuere Kernel wie 4.13 verfügen über eBPF für die Profilerstellung. Siehe brendangregg.com/blog/2015-05-15/ebpf-one-small-step.html und brendangregg.com/ebpf.html
Andrew Stern

Eine weitere nette Einführung perfgibt es unter archive.li/9r927#selection-767.126-767.271 (Warum die SO-Götter beschlossen haben, diese Seite aus der SO-Wissensdatenbank zu löschen, ist mir ein
Rätsel

75

Ich würde Valgrind und Callgrind als Basis für meine Profiling-Tool-Suite verwenden. Es ist wichtig zu wissen, dass Valgrind im Grunde eine virtuelle Maschine ist:

(wikipedia) Valgrind ist im Wesentlichen eine virtuelle Maschine, die Just-in-Time-Kompilierungstechniken (JIT) verwendet, einschließlich dynamischer Neukompilierung. Nichts aus dem ursprünglichen Programm wird jemals direkt auf dem Host-Prozessor ausgeführt. Stattdessen übersetzt Valgrind das Programm zunächst in eine temporäre, einfachere Form namens Intermediate Representation (IR), eine prozessorneutrale, SSA-basierte Form. Nach der Konvertierung kann ein Tool (siehe unten) alle gewünschten Transformationen im IR durchführen, bevor Valgrind das IR wieder in Maschinencode übersetzt und vom Host-Prozessor ausführen lässt.

Callgrind ist ein Profiler, der darauf aufbaut. Der Hauptvorteil besteht darin, dass Sie Ihre Anwendung nicht stundenlang ausführen müssen, um ein zuverlässiges Ergebnis zu erzielen. Schon ein Durchlauf von einer Sekunde reicht aus, um solide und zuverlässige Ergebnisse zu erzielen, da Callgrind ein nicht prüfender Profiler ist.

Ein weiteres Werkzeug, das auf Valgrind aufbaut, ist Massif. Ich benutze es, um die Nutzung des Heapspeichers zu profilieren. Es funktioniert großartig. Was es tut, ist, dass es Ihnen Schnappschüsse der Speichernutzung gibt - detaillierte Informationen WAS hält WAS Prozentsatz des Speichers, und die WHO hat es dort abgelegt. Diese Informationen sind zu verschiedenen Zeitpunkten der Anwendungsausführung verfügbar.


70

Die Antwort zum Ausführen valgrind --tool=callgrindist ohne einige Optionen nicht vollständig. Normalerweise möchten wir unter Valgrind keine 10 Minuten langsame Startzeit profilieren und unser Programm profilieren, wenn es eine Aufgabe ausführt.

Das empfehle ich also. Führen Sie zuerst das Programm aus:

valgrind --tool=callgrind --dump-instr=yes -v --instr-atstart=no ./binary > tmp

Wenn es jetzt funktioniert und wir mit der Profilerstellung beginnen möchten, sollten wir es in einem anderen Fenster ausführen:

callgrind_control -i on

Dadurch wird die Profilerstellung aktiviert. Um es auszuschalten und die gesamte Aufgabe zu stoppen, könnten wir Folgendes verwenden:

callgrind_control -k

Jetzt haben wir einige Dateien mit dem Namen callgrind.out. * Im aktuellen Verzeichnis. Verwenden Sie zum Anzeigen der Profilerstellungsergebnisse:

kcachegrind callgrind.out.*

Ich empfehle im nächsten Fenster auf "Self" zu klicken, andernfalls wird angezeigt, dass "main ()" die zeitaufwändigste Aufgabe ist. "Selbst" zeigt, wie viel Zeit jede Funktion selbst benötigt hat, nicht zusammen mit abhängigen Personen.


9
Aus irgendeinem Grund waren callgrind.out. * -Dateien immer leer. Das Ausführen von callgrind_control -d war nützlich, um das Speichern von Daten auf der Festplatte zu erzwingen.
Tõnu Samuel

3
Kippen. Meine üblichen Kontexte sind so etwas wie ganzes MySQL oder PHP oder eine ähnliche große Sache. Oft weiß ich gar nicht, was ich zuerst trennen möchte.
Tõnu Samuel

2
In meinem Fall lädt mein Programm tatsächlich eine Reihe von Daten in einen LRU-Cache, und ich möchte das nicht profilieren. Daher erzwinge ich beim Start das Laden einer Teilmenge des Caches und profiliere den Code nur anhand dieser Daten (damit OS + CPU die Speichernutzung in meinem Cache verwalten kann). Es funktioniert, aber das Laden dieses Caches ist langsam und CPU-intensiv für Code, den ich in einem anderen Kontext profilieren möchte, sodass Callgrind stark verschmutzte Ergebnisse liefert.
Code Abominator

2
Es besteht auch CALLGRIND_TOGGLE_COLLECTdie Möglichkeit, die Sammlung programmgesteuert zu aktivieren / deaktivieren. siehe stackoverflow.com/a/13700817/288875
Andre Holzner

1
Wow, ich wusste nicht, dass es das gibt, danke!
Vincent Fourmond

59

Dies ist eine Antwort auf Nazgobs Gprof-Antwort .

Ich habe Gprof in den letzten Tagen verwendet und bereits drei signifikante Einschränkungen festgestellt, von denen ich (noch) nirgendwo anders dokumentiert gesehen habe:

  1. Bei Multithread-Code funktioniert dies nicht ordnungsgemäß, es sei denn, Sie verwenden eine Problemumgehung

  2. Das Aufrufdiagramm wird durch Funktionszeiger verwirrt. Beispiel: Ich habe eine aufgerufene Funktion, mit multithread()der ich eine bestimmte Funktion über ein bestimmtes Array (beide als Argumente übergeben) mit mehreren Threads versehen kann. Gprof sieht jedoch alle Anrufe anmultithread() als gleichwertig, um die bei Kindern verbrachte Zeit zu berechnen. Da einige Funktionen multithread()viel länger dauern als andere, sind meine Anrufdiagramme meistens unbrauchbar. (Für diejenigen, die sich fragen, ob Threading hier das Problem ist: Nein, multithread()kann optional und in diesem Fall alles nacheinander nur auf dem aufrufenden Thread ausführen).

  3. Es heißt hier , dass „... die Nummer-of-Anrufe Zahlen durch Zählen abgeleitet sind, nicht abgetastet wird . Sie völlig korrekt ist ...“. Mein Anrufdiagramm gibt mir jedoch 5345859132 + 784984078 als Anrufstatistik für meine am häufigsten genannte Funktion, bei der die erste Nummer Direktanrufe sein sollen, und die zweiten rekursiven Anrufe (die alle von sich selbst stammen). Da dies implizierte, dass ich einen Fehler hatte, habe ich lange (64-Bit) Zähler in den Code eingefügt und den gleichen Lauf erneut ausgeführt. Meine Anzahl: 5345859132 direkte und 78094395406 selbstrekursive Anrufe. Da es dort viele Ziffern gibt, möchte ich darauf hinweisen, dass die von mir gemessenen rekursiven Aufrufe 78 Mrd. gegenüber 784 m von Gprof betragen: ein Faktor von 100, der sich unterscheidet. Beide Läufe waren Single-Threaded-Code und nicht optimierter Code, einer kompiliert -gund der andere -pg.

Dies war GNU Gprof (GNU Binutils für Debian) 2.18.0.20080103, das unter 64-Bit-Debian Lenny ausgeführt wurde, wenn dies jemandem hilft.


Ja, es wird eine Stichprobe erstellt, jedoch nicht für die Anzahl der Anrufe. Interessanterweise führte das Folgen Ihres Links letztendlich zu einer aktualisierten Version der Handbuchseite, auf die ich in meinem Beitrag verlinkt habe: Neue URL: sourceware.org/binutils/docs/gprof/… Dies wiederholt das Zitat in Teil (iii) meiner Antwort. aber sagt auch "In Multithread-Anwendungen oder Single-Thread-Anwendungen, die mit Multithread-Bibliotheken verknüpft sind, sind die Zählungen nur dann deterministisch, wenn die Zählfunktion threadsicher ist. (Hinweis: Beachten Sie, dass die mcount-Zählfunktion in glibc kein Thread ist -sicher)."
Rob_before_edits

Es ist mir nicht klar, ob dies mein Ergebnis in (iii) erklärt. Mein Code wurde mit -lpthread -lm verknüpft und sowohl als statische Variable "pthread_t * thr" als auch als statische Variable "pthread_mutex_t nextLock = PTHREAD_MUTEX_INITIALIZER" deklariert, selbst wenn ein einzelner Thread ausgeführt wurde. Ich würde normalerweise annehmen, dass "Verknüpfung mit Multithread-Bibliotheken" bedeutet, diese Bibliotheken tatsächlich zu verwenden, und zwar in größerem Umfang, aber ich könnte mich irren!
Rob_before_edits

23

Verwenden Sie Valgrind, Callgrind und Kcachegrind:

valgrind --tool=callgrind ./(Your binary)

generiert callgrind.out.x. Lesen Sie es mit kcachegrind.

Verwenden Sie gprof (add -pg):

cc -o myprog myprog.c utils.c -g -pg 

(nicht so gut für Multithreads, Funktionszeiger)

Verwenden Sie Google-Perftools:

Verwendet Zeitabtastung, E / A- und CPU-Engpässe werden aufgedeckt.

Intel VTune ist das Beste (kostenlos für Bildungszwecke).

Andere: AMD Codeanalyst (seitdem durch AMD CodeXL ersetzt), OProfile, 'perf'-Tools (apt-get install linux-tools)


10

Übersicht über C ++ - Profiling-Techniken

In dieser Antwort werde ich verschiedene Tools verwenden, um einige sehr einfache Testprogramme zu analysieren und die Funktionsweise dieser Tools konkret zu vergleichen.

Das folgende Testprogramm ist sehr einfach und führt Folgendes aus:

  • mainAnrufe fastund maybe_slow3 Mal, wobei einer der maybe_slowAnrufe langsam ist

    Der langsame Aufruf von maybe_slowist 10x länger und dominiert die Laufzeit, wenn wir Aufrufe der untergeordneten Funktion berücksichtigen common. Im Idealfall kann das Profiling-Tool uns auf den spezifischen langsamen Aufruf verweisen.

  • beides fastund maybe_slowcall common, was den Großteil der Programmausführung ausmacht

  • Die Programmoberfläche ist:

    ./main.out [n [seed]]

    und das Programm macht O(n^2)insgesamt Schleifen. seeddient nur dazu, eine andere Ausgabe zu erhalten, ohne die Laufzeit zu beeinträchtigen.

Haupt c

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>

uint64_t __attribute__ ((noinline)) common(uint64_t n, uint64_t seed) {
    for (uint64_t i = 0; i < n; ++i) {
        seed = (seed * seed) - (3 * seed) + 1;
    }
    return seed;
}

uint64_t __attribute__ ((noinline)) fast(uint64_t n, uint64_t seed) {
    uint64_t max = (n / 10) + 1;
    for (uint64_t i = 0; i < max; ++i) {
        seed = common(n, (seed * seed) - (3 * seed) + 1);
    }
    return seed;
}

uint64_t __attribute__ ((noinline)) maybe_slow(uint64_t n, uint64_t seed, int is_slow) {
    uint64_t max = n;
    if (is_slow) {
        max *= 10;
    }
    for (uint64_t i = 0; i < max; ++i) {
        seed = common(n, (seed * seed) - (3 * seed) + 1);
    }
    return seed;
}

int main(int argc, char **argv) {
    uint64_t n, seed;
    if (argc > 1) {
        n = strtoll(argv[1], NULL, 0);
    } else {
        n = 1;
    }
    if (argc > 2) {
        seed = strtoll(argv[2], NULL, 0);
    } else {
        seed = 0;
    }
    seed += maybe_slow(n, seed, 0);
    seed += fast(n, seed);
    seed += maybe_slow(n, seed, 1);
    seed += fast(n, seed);
    seed += maybe_slow(n, seed, 0);
    seed += fast(n, seed);
    printf("%" PRIX64 "\n", seed);
    return EXIT_SUCCESS;
}

gprof

gprof erfordert das Neukompilieren der Software mit Instrumentierung und verwendet zusammen mit dieser Instrumentierung einen Stichprobenansatz. Es wird daher ein Gleichgewicht zwischen Genauigkeit (Abtastung ist nicht immer vollständig genau und kann Funktionen überspringen) und Ausführungsverlangsamung (Instrumentierung und Abtastung sind relativ schnelle Techniken, die die Ausführung nicht sehr verlangsamen) hergestellt.

gprof ist in GCC / binutils integriert. Wir müssen also nur mit der -pgOption kompilieren , um gprof zu aktivieren. Wir führen das Programm dann normal mit einem CLI-Parameter der Größe aus, der einen Lauf mit einer angemessenen Dauer von einigen Sekunden erzeugt ( 10000):

gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time ./main.out 10000

Aus pädagogischen Gründen werden wir auch einen Lauf ohne aktivierte Optimierungen durchführen. Beachten Sie, dass dies in der Praxis nutzlos ist, da Sie normalerweise nur die Leistung des optimierten Programms optimieren möchten:

gcc -pg -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out 10000

Erstens timesagt uns, dass die Ausführungszeit mit und ohne -pggleich war, was großartig ist: keine Verlangsamung! Ich habe jedoch Berichte über 2x - 3x Verlangsamungen bei komplexer Software gesehen, z. B. wie in diesem Ticket gezeigt .

Da wir mit kompiliert haben -pg, erzeugt das Ausführen des Programms eine Dateigmon.out erstellt, die die Profildaten enthält.

Wir können diese Datei grafisch beobachten, indem wir gprof2dotgefragt werden: Ist es möglich, eine grafische Darstellung der gprof-Ergebnisse zu erhalten?

sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof main.out > main.gprof
gprof2dot < main.gprof | dot -Tsvg -o output.svg

Hier gprofliest das Tool die gmon.outTrace-Informationen und generiert einen von Menschen lesbaren Bericht, in main.gprofdemgprof2dot dann liest, um ein Diagramm zu generieren.

Die Quelle für gprof2dot ist: https://github.com/jrfonseca/gprof2dot

Wir beobachten Folgendes für den -O0Lauf:

Geben Sie hier die Bildbeschreibung ein

und für den -O3Lauf:

Geben Sie hier die Bildbeschreibung ein

Die -O0Ausgabe ist ziemlich selbsterklärend. Zum Beispiel zeigt es, dass die 3 maybe_slowAufrufe und ihre untergeordneten Aufrufe 97,56% der gesamten Laufzeit ausmachen, obwohl die Ausführung maybe_slowohne untergeordnete Aufrufe 0,00% der gesamten Ausführungszeit ausmacht, dh fast die gesamte Zeit, die für diese Funktion aufgewendet wurde Kinderanrufe.

TODO: Warum mainfehlt in der -O3Ausgabe, obwohl ich es auf einem btin GDB sehen kann? Fehlende Funktion in der GProf-Ausgabe Ich denke, das liegt daran, dass gprof neben seiner kompilierten Instrumentierung auch Sampling-basierte Funktionen verwendet. Das -O3 mainist einfach zu schnell und hat keine Samples.

Ich wähle SVG-Ausgabe anstelle von PNG, da die SVG mit Strg + F durchsucht werden kann und die Dateigröße etwa 10x kleiner sein kann. Außerdem kann die Breite und Höhe des generierten Bildes mit Zehntausenden von Pixeln für komplexe Software eogenorm sein , und GNOME 3.28.1 tritt in diesem Fall bei PNGs auf, während SVGs von meinem Browser automatisch geöffnet werden. Gimp 2.8 hat aber gut funktioniert, siehe auch:

Aber selbst dann ziehen Sie das Bild viel herum, um zu finden, was Sie wollen. Sehen Sie sich z. B. dieses Bild aus einem "echten" Softwarebeispiel aus diesem Ticket an :

Geben Sie hier die Bildbeschreibung ein

Können Sie den kritischsten Anrufstapel leicht finden, wenn all diese winzigen unsortierten Spaghetti-Linien übereinander gehen? dotIch bin mir sicher, dass es bessere Optionen gibt, aber ich möchte jetzt nicht dorthin gehen. Was wir wirklich brauchen, ist ein engagierter Betrachter dafür, aber ich habe noch keinen gefunden:

Sie können jedoch die Farbkarte verwenden, um diese Probleme ein wenig zu verringern. Zum Beispiel habe ich es auf dem vorherigen großen Bild endlich geschafft, den kritischen Pfad auf der linken Seite zu finden, als ich den brillanten Schluss gezogen habe, dass Grün nach Rot kommt, gefolgt von immer dunklerem Blau.

Alternativ können wir auch die Textausgabe des gprofintegrierten binutils-Tools beobachten, das wir zuvor gespeichert haben unter:

cat main.gprof

Standardmäßig wird eine äußerst ausführliche Ausgabe erstellt, die erklärt, was die Ausgabedaten bedeuten. Da ich es nicht besser erklären kann, lasse ich Sie es selbst lesen.

Sobald Sie das Datenausgabeformat verstanden haben, können Sie die Ausführlichkeit reduzieren, um nur die Daten ohne das Lernprogramm mit der folgenden -bOption anzuzeigen:

gprof -b main.out

In unserem Beispiel waren die Ausgaben für -O0:

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls   s/call   s/call  name    
100.35      3.67     3.67   123003     0.00     0.00  common
  0.00      3.67     0.00        3     0.00     0.03  fast
  0.00      3.67     0.00        3     0.00     1.19  maybe_slow

            Call graph


granularity: each sample hit covers 2 byte(s) for 0.27% of 3.67 seconds

index % time    self  children    called     name
                0.09    0.00    3003/123003      fast [4]
                3.58    0.00  120000/123003      maybe_slow [3]
[1]    100.0    3.67    0.00  123003         common [1]
-----------------------------------------------
                                                 <spontaneous>
[2]    100.0    0.00    3.67                 main [2]
                0.00    3.58       3/3           maybe_slow [3]
                0.00    0.09       3/3           fast [4]
-----------------------------------------------
                0.00    3.58       3/3           main [2]
[3]     97.6    0.00    3.58       3         maybe_slow [3]
                3.58    0.00  120000/123003      common [1]
-----------------------------------------------
                0.00    0.09       3/3           main [2]
[4]      2.4    0.00    0.09       3         fast [4]
                0.09    0.00    3003/123003      common [1]
-----------------------------------------------

Index by function name

   [1] common                  [4] fast                    [3] maybe_slow

und für -O3:

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  us/call  us/call  name    
100.52      1.84     1.84   123003    14.96    14.96  common

            Call graph


granularity: each sample hit covers 2 byte(s) for 0.54% of 1.84 seconds

index % time    self  children    called     name
                0.04    0.00    3003/123003      fast [3]
                1.79    0.00  120000/123003      maybe_slow [2]
[1]    100.0    1.84    0.00  123003         common [1]
-----------------------------------------------
                                                 <spontaneous>
[2]     97.6    0.00    1.79                 maybe_slow [2]
                1.79    0.00  120000/123003      common [1]
-----------------------------------------------
                                                 <spontaneous>
[3]      2.4    0.00    0.04                 fast [3]
                0.04    0.00    3003/123003      common [1]
-----------------------------------------------

Index by function name

   [1] common

Als sehr schnelle Zusammenfassung für jeden Abschnitt zB:

                0.00    3.58       3/3           main [2]
[3]     97.6    0.00    3.58       3         maybe_slow [3]
                3.58    0.00  120000/123003      common [1]

zentriert sich um die Funktion, die eingerückt bleibt ( maybe_flow). [3]ist die ID dieser Funktion. Über der Funktion befinden sich die Anrufer und darunter die Callees.

Für -O3sehen, hier wie in der grafischen Ausgabe , dass maybe_slowund fastkeinen bekannten Elternteil hat, das ist das, was in der Dokumentation, sagt <spontaneous>Mittel.

Ich bin mir nicht sicher, ob es eine gute Möglichkeit gibt, mit gprof zeilenweise Profile zu erstellen: `gprof` Zeit, die in bestimmten Codezeilen verbracht wird

valgrind callgrind

valgrind führt das Programm über die virtuelle Maschine valgrind aus. Dies macht die Profilerstellung sehr genau, führt aber auch zu einer sehr starken Verlangsamung des Programms. Ich habe kcachegrind auch bereits erwähnt unter: Tools zum Abrufen eines Diagramms für Bildaufrufe von Code

callgrind ist das Werkzeug von valgrind zum Profilieren von Code und kcachegrind ist ein KDE-Programm, das die Cachegrind-Ausgabe visualisieren kann.

Zuerst müssen wir das -pgFlag entfernen , um zur normalen Kompilierung zurückzukehren, andernfalls schlägt der Lauf tatsächlich mit fehlProfiling timer expired , und ja, dies ist so häufig, dass ich es getan habe, und es gab eine Frage zum Stapelüberlauf.

Also kompilieren und laufen wir wie folgt:

sudo apt install kcachegrind valgrind
gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time valgrind --tool=callgrind valgrind --dump-instr=yes \
  --collect-jumps=yes ./main.out 10000

Ich aktiviere --dump-instr=yes --collect-jumps=yes weil dadurch auch Informationen ausgegeben werden, die es uns ermöglichen, eine Aufschlüsselung der Leistung pro Fließband bei relativ geringen zusätzlichen Gemeinkosten anzuzeigen.

Auf Anhieb wurde timeuns mitgeteilt, dass die Ausführung des Programms 29,5 Sekunden dauerte, sodass wir in diesem Beispiel eine etwa 15-fache Verlangsamung hatten. Diese Verlangsamung wird eindeutig eine ernsthafte Einschränkung für größere Workloads darstellen. Bei dem hier erwähnten "realen Softwarebeispiel" habe ich eine Verlangsamung von 80x beobachtet.

Der Lauf generiert eine Profildatendatei mit dem Namen callgrind.out.<pid>zB callgrind.out.8554in meinem Fall. Wir sehen diese Datei mit:

kcachegrind callgrind.out.8554

Dies zeigt eine GUI, die Daten enthält, die der Textausgabe von gprof ähneln:

Geben Sie hier die Bildbeschreibung ein

Wenn wir unten rechts auf die Registerkarte "Anrufdiagramm" gehen, sehen wir ein Anrufdiagramm, das wir exportieren können, indem wir mit der rechten Maustaste darauf klicken, um das folgende Bild mit unangemessenen weißen Rändern zu erhalten :-)

Geben Sie hier die Bildbeschreibung ein

Ich denke, fastwird in diesem Diagramm nicht angezeigt, da kcachegrind die Visualisierung vereinfacht haben muss, da dieser Aufruf zu wenig Zeit in Anspruch nimmt. Dies ist wahrscheinlich das Verhalten, das Sie für ein echtes Programm wünschen. Das Rechtsklick-Menü enthält einige Einstellungen, mit denen gesteuert werden kann, wann solche Knoten entfernt werden sollen. Nach einem kurzen Versuch konnte ich jedoch keinen so kurzen Anruf anzeigen. Wenn ich fastauf das linke Fenster klicke, wird ein Anrufdiagramm mit angezeigt fast, sodass der Stapel tatsächlich erfasst wurde. Bisher hatte noch niemand eine Möglichkeit gefunden, das vollständige Diagrammaufrufdiagramm anzuzeigen : Lassen Sie callgrind alle Funktionsaufrufe im kcachegrind-Aufrufgraphen anzeigen

TODO auf komplexer C ++ - Software sehe ich einige Einträge vom Typ <cycle N>, z. B. <cycle 11>wo ich Funktionsnamen erwarten würde, was bedeutet das? Ich habe festgestellt, dass es eine Schaltfläche "Zykluserkennung" gibt, mit der Sie das ein- und ausschalten können. Aber was bedeutet das?

perf von linux-tools

perfscheint ausschließlich Linux-Kernel-Sampling-Mechanismen zu verwenden. Dies macht es sehr einfach einzurichten, aber auch nicht ganz genau.

sudo apt install linux-tools
time perf record -g ./main.out 10000

Dies hat die Ausführung um 0,2 Sekunden verlängert, so dass wir zeitlich in Ordnung sind, aber ich sehe immer noch kein großes Interesse, nachdem ich den commonKnoten mit dem rechten Pfeil der Tastatur erweitert habe:

Samples: 7K of event 'cycles:uppp', Event count (approx.): 6228527608     
  Children      Self  Command   Shared Object     Symbol                  
-   99.98%    99.88%  main.out  main.out          [.] common              
     common                                                               
     0.11%     0.11%  main.out  [kernel]          [k] 0xffffffff8a6009e7  
     0.01%     0.01%  main.out  [kernel]          [k] 0xffffffff8a600158  
     0.01%     0.00%  main.out  [unknown]         [k] 0x0000000000000040  
     0.01%     0.00%  main.out  ld-2.27.so        [.] _dl_sysdep_start    
     0.01%     0.00%  main.out  ld-2.27.so        [.] dl_main             
     0.01%     0.00%  main.out  ld-2.27.so        [.] mprotect            
     0.01%     0.00%  main.out  ld-2.27.so        [.] _dl_map_object      
     0.01%     0.00%  main.out  ld-2.27.so        [.] _xstat              
     0.00%     0.00%  main.out  ld-2.27.so        [.] __GI___tunables_init
     0.00%     0.00%  main.out  [unknown]         [.] 0x2f3d4f4944555453  
     0.00%     0.00%  main.out  [unknown]         [.] 0x00007fff3cfc57ac  
     0.00%     0.00%  main.out  ld-2.27.so        [.] _start              

Dann versuche ich, das -O0Programm zu vergleichen, um festzustellen, ob dies etwas anzeigt, und erst jetzt sehe ich endlich ein Aufrufdiagramm:

Samples: 15K of event 'cycles:uppp', Event count (approx.): 12438962281   
  Children      Self  Command   Shared Object     Symbol                  
+   99.99%     0.00%  main.out  [unknown]         [.] 0x04be258d4c544155  
+   99.99%     0.00%  main.out  libc-2.27.so      [.] __libc_start_main   
-   99.99%     0.00%  main.out  main.out          [.] main                
   - main                                                                 
      - 97.54% maybe_slow                                                 
           common                                                         
      - 2.45% fast                                                        
           common                                                         
+   99.96%    99.85%  main.out  main.out          [.] common              
+   97.54%     0.03%  main.out  main.out          [.] maybe_slow          
+    2.45%     0.00%  main.out  main.out          [.] fast                
     0.11%     0.11%  main.out  [kernel]          [k] 0xffffffff8a6009e7  
     0.00%     0.00%  main.out  [unknown]         [k] 0x0000000000000040  
     0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_sysdep_start    
     0.00%     0.00%  main.out  ld-2.27.so        [.] dl_main             
     0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_lookup_symbol_x 
     0.00%     0.00%  main.out  [kernel]          [k] 0xffffffff8a600158  
     0.00%     0.00%  main.out  ld-2.27.so        [.] mmap64              
     0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_map_object      
     0.00%     0.00%  main.out  ld-2.27.so        [.] __GI___tunables_init
     0.00%     0.00%  main.out  [unknown]         [.] 0x552e53555f6e653d  
     0.00%     0.00%  main.out  [unknown]         [.] 0x00007ffe1cf20fdb  
     0.00%     0.00%  main.out  ld-2.27.so        [.] _start              

TODO: Was ist bei der -O3Hinrichtung passiert ? Ist es einfach so maybe_slowund fastwaren zu schnell und haben keine Proben bekommen? Funktioniert es gut mit -O3größeren Programmen, deren Ausführung länger dauert? Habe ich eine CLI-Option verpasst? Ich habe herausgefunden -F, dass ich die Abtastfrequenz in Hertz steuern soll, aber ich habe sie auf das maximal zulässige Maximum von -F 39500(könnte mit erhöht werden sudo) eingestellt und sehe immer noch keine eindeutigen Anrufe.

Eine coole Sache perfist das FlameGraph-Tool von Brendan Gregg, das die Anrufstapel-Timings auf sehr übersichtliche Weise anzeigt, sodass Sie die großen Anrufe schnell sehen können. Das Tool ist verfügbar unter: https://github.com/brendangregg/FlameGraph und wird auch auf seinem perf Tutorial erwähnt unter: http://www.brendangregg.com/perf.html#FlameGraphs Als ich lief perfohne sudoIch habe ERROR: No stack counts foundso für jetzt mache ich es mit sudo:

git clone https://github.com/brendangregg/FlameGraph
sudo perf record -F 99 -g -o perf_with_stack.data ./main.out 10000
sudo perf script -i perf_with_stack.data | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > flamegraph.svg

aber in einem so einfachen Programm ist die Ausgabe nicht sehr leicht zu verstehen, da wir weder maybe_slownoch fastin dieser Grafik leicht sehen können :

Geben Sie hier die Bildbeschreibung ein

An einem komplexeren Beispiel wird deutlich, was die Grafik bedeutet:

Geben Sie hier die Bildbeschreibung ein

TODO gibt es ein Protokoll von [unknown] in diesem Beispiel Funktionsprotokoll, warum ist das so?

Eine weitere perfekte GUI-Oberfläche, die sich lohnen könnte, sind:

  • Eclipse Trace Compass Plugin: https://www.eclipse.org/tracecompass/

    Dies hat jedoch den Nachteil, dass Sie zuerst die Daten in das Common Trace-Format konvertieren müssen, was zwar möglich ist perf data --to-ctf, aber zum Zeitpunkt der Erstellung aktiviert sein muss / perfneu genug sein muss, was für die Ausführung nicht der Fall ist Ubuntu 18.04

  • https://github.com/KDAB/hotspot

    Der Nachteil dabei ist, dass es anscheinend kein Ubuntu-Paket gibt und für dessen Erstellung Qt 5.10 erforderlich ist, während Ubuntu 18.04 bei Qt 5.9 ist.

gperftools

Zuvor als "Google Performance Tools" bezeichnet, Quelle: https://github.com/gperftools/gperftools Beispielbasiert.

Installieren Sie zuerst gperftools mit:

sudo apt install google-perftools

Anschließend können wir den gperftools-CPU-Profiler auf zwei Arten aktivieren: zur Laufzeit oder zur Erstellungszeit.

Zur Laufzeit müssen wir set the LD_PRELOADto point übergeben libprofiler.so, mit dem Sie locate libprofiler.sozB auf meinem System finden können:

gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so \
  CPUPROFILE=prof.out ./main.out 10000

Alternativ können wir die Bibliothek zur Verbindungszeit einbauen und LD_PRELOADzur Laufzeit auf Folgendes verzichten:

gcc -Wl,--no-as-needed,-lprofiler,--as-needed -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
CPUPROFILE=prof.out ./main.out 10000

Siehe auch: gperftools - Profildatei nicht gesichert

Der schönste Weg, diese Daten anzuzeigen, die ich bisher gefunden habe, besteht darin, die Ausgabe von pprof auf dasselbe Format zu bringen, das kcachegrind als Eingabe verwendet (ja, das Valgrind-Projekt-Viewer-Tool), und kcachegrind zu verwenden, um Folgendes anzuzeigen:

google-pprof --callgrind main.out prof.out  > callgrind.out
kcachegrind callgrind.out

Nach dem Ausführen mit einer dieser Methoden erhalten wir eine prof.outProfildatendatei als Ausgabe. Wir können diese Datei grafisch als SVG anzeigen mit:

google-pprof --web main.out prof.out

Geben Sie hier die Bildbeschreibung ein

Dies ergibt wie andere Tools ein vertrautes Aufrufdiagramm, jedoch mit der klobigen Einheit der Anzahl der Samples anstelle von Sekunden.

Alternativ können wir auch einige Textdaten erhalten mit:

google-pprof --text main.out prof.out

was gibt:

Using local file main.out.
Using local file prof.out.
Total: 187 samples
     187 100.0% 100.0%      187 100.0% common
       0   0.0% 100.0%      187 100.0% __libc_start_main
       0   0.0% 100.0%      187 100.0% _start
       0   0.0% 100.0%        4   2.1% fast
       0   0.0% 100.0%      187 100.0% main
       0   0.0% 100.0%      183  97.9% maybe_slow

Siehe auch: So verwenden Sie Google Perf Tools

Getestet in Ubuntu 18.04, gprof2dot 2019.11.30, valgrind 3.13.0, perf 4.15.18, Linux-Kernel 4.15.0, FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b, gperftools 2.5-2.


2
Standardmäßig verwendet perf record das Frame-Zeigerregister. Moderne Compiler zeichnen die Frame-Adresse nicht auf und verwenden stattdessen das Register als allgemeinen Zweck. Die Alternative besteht darin, mit -fno-omit-frame-pointerflag zu kompilieren oder eine andere Alternative zu verwenden: Aufzeichnen mit --call-graph "dwarf"oder --call-graph "lbr"abhängig von Ihrem Szenario.
Jorge Bellon

5

Für Single-Thread-Programme können Sie igprof , The Ignominous Profiler, verwenden: https://igprof.org/ .

Es handelt sich um einen Stichprobenprofiler nach dem Vorbild der ... langen ... Antwort von Mike Dunlavey, der die Ergebnisse in einem durchsuchbaren Aufrufstapelbaum verpackt, der mit der Zeit oder dem Speicher der einzelnen Funktionen versehen ist, entweder kumulativ oder pro Funktion.


Es sieht interessant aus, kann aber nicht mit GCC 9.2 kompiliert werden. (Debian / Sid) Ich habe ein Problem mit Github gemacht.
Basile Starynkevitch

5

Erwähnenswert sind auch

  1. HPCToolkit ( http://hpctoolkit.org/ ) - Open Source, funktioniert für parallele Programme und verfügt über eine grafische Benutzeroberfläche , mit der Sie die Ergebnisse auf verschiedene Arten anzeigen können
  2. Intel VTune ( https://software.intel.com/en-us/vtune ) - Wenn Sie Intel-Compiler haben, ist dies sehr gut
  3. TAU ( http://www.cs.uoregon.edu/research/tau/home.php )

Ich habe HPCToolkit und VTune verwendet und sie sind sehr effektiv beim Auffinden der langen Stange im Zelt und müssen Ihren Code nicht neu kompiliert werden (außer dass Sie den in CMake erstellten Typ -g -O oder RelWithDebInfo verwenden müssen, um eine aussagekräftige Ausgabe zu erhalten). . Ich habe gehört, TAU ist in seinen Fähigkeiten ähnlich.


4

Dies sind die beiden Methoden, mit denen ich meinen Code beschleunige:

Für CPU-gebundene Anwendungen:

  1. Verwenden Sie einen Profiler im DEBUG-Modus, um fragwürdige Teile Ihres Codes zu identifizieren
  2. Wechseln Sie dann in den RELEASE-Modus und kommentieren Sie die fraglichen Abschnitte Ihres Codes aus (stubben Sie ihn mit nichts), bis Sie Leistungsänderungen feststellen.

Für E / A-gebundene Anwendungen:

  1. Verwenden Sie einen Profiler im RELEASE-Modus, um fragwürdige Teile Ihres Codes zu identifizieren.

NB

Wenn Sie keinen Profiler haben, verwenden Sie den Profiler des armen Mannes. Klicken Sie beim Debuggen Ihrer Anwendung auf Pause. Die meisten Entwicklersuiten werden mit kommentierten Zeilennummern in Assemblys aufgeteilt. Es ist statistisch wahrscheinlich, dass Sie in einer Region landen, die den größten Teil Ihrer CPU-Zyklen verbraucht.

Für die CPU liegt der Grund für die Profilerstellung im DEBUG- Modus darin, dass Sie versucht haben, die Profilerstellung in RELEASE durchzuführen , dass der Compiler, Modus , die Mathematik reduziert, Schleifen vektorisiert und Inline-Funktionen verwendet, die dazu führen, dass Ihr Code beim Zusammenstellen in ein nicht zuordnbares Durcheinander zerfällt. Ein nicht zuordnbares Durcheinander bedeutet, dass Ihr Profiler nicht eindeutig erkennen kann, was so lange dauert, da die Assembly möglicherweise nicht dem zu optimierenden Quellcode entspricht . Wenn Sie die Leistung (z. B. zeitabhängig) des RELEASE- Modus benötigen , deaktivieren Sie die Debugger-Funktionen nach Bedarf, um eine nutzbare Leistung zu erhalten.

Bei E / A-Bindung kann der Profiler E / A-Vorgänge im RELEASE- Modus weiterhin identifizieren, da E / A-Vorgänge entweder extern (meistens) mit einer gemeinsam genutzten Bibliothek verknüpft sind oder im schlimmsten Fall zu einem System führen. Interrupt-Vektor aufrufen (der auch vom Profiler leicht identifiziert werden kann).


2
+1 Die Methode des armen Mannes funktioniert für E / A-gebunden genauso gut wie für CPU-gebunden, und ich empfehle, alle Leistungsoptimierungen im DEBUG-Modus durchzuführen. Wenn Sie mit dem Einstellen fertig sind, aktivieren Sie RELEASE. Es wird eine Verbesserung bewirken, wenn das Programm in Ihrem Code CPU-gebunden ist. Hier ist ein grobes, aber kurzes Video des Prozesses.
Mike Dunlavey

3
Ich würde DEBUG-Builds nicht für die Leistungsprofilerstellung verwenden. Oft habe ich gesehen, dass leistungskritische Teile im DEBUG-Modus im Release-Modus vollständig optimiert sind. Ein weiteres Problem ist die Verwendung von Asserts im Debug-Code, die die Leistung beeinträchtigen.
Gast128

3
Hast du meinen Beitrag überhaupt gelesen? "Wenn Sie die Leistung (z. B. zeitabhängig) des RELEASE-Modus benötigen, deaktivieren Sie die Debugger-Funktionen nach Bedarf, um eine nutzbare Leistung zu erhalten.", "Wechseln Sie dann in den RELEASE-Modus und kommentieren Sie die fraglichen Abschnitte Ihres Codes (Stub it with nothing), bis Sie sehen Leistungsänderungen. "? Ich sagte, nach möglichen Problembereichen im Debug-Modus suchen und diese Probleme im Release-Modus überprüfen, um die von Ihnen erwähnte Gefahr zu vermeiden.
seo


2

Sie können ein Protokollierungsframework wie das folgende verwenden, loguruda es Zeitstempel und Gesamtverfügbarkeit enthält, die sich gut für die Profilerstellung verwenden lassen:


1

Bei der Arbeit haben wir ein wirklich nettes Tool, mit dem wir überwachen können, was wir in Bezug auf die Planung wollen. Dies war mehrfach nützlich.

Es ist in C ++ und muss an Ihre Bedürfnisse angepasst werden. Leider kann ich keinen Code teilen, nur Konzepte. Sie verwenden einen "großen" volatilePuffer mit Zeitstempeln und Ereignis-ID, den Sie post mortem oder nach dem Stoppen des Protokollierungssystems sichern können (und diesen beispielsweise in eine Datei speichern können).

Sie rufen den sogenannten großen Puffer mit allen Daten ab und eine kleine Schnittstelle analysiert ihn und zeigt Ereignisse mit Namen (auf / ab + Wert) an, wie es ein Oszilloskop mit Farben tut (in .hppDatei konfiguriert ).

Sie passen die Anzahl der generierten Ereignisse so an, dass sie sich ausschließlich auf das konzentrieren, was Sie möchten. Dies hat uns bei der Planung von Problemen sehr geholfen, während die benötigte CPU-Menge basierend auf der Anzahl der protokollierten Ereignisse pro Sekunde verbraucht wurde.

Sie benötigen 3 Dateien:

toolname.hpp // interface
toolname.cpp // code
tool_events_id.hpp // Events ID

Das Konzept besteht darin, Ereignisse folgendermaßen zu definieren tool_events_id.hpp:

// EVENT_NAME                         ID      BEGIN_END BG_COLOR NAME
#define SOCK_PDU_RECV_D               0x0301  //@D00301 BGEEAAAA # TX_PDU_Recv
#define SOCK_PDU_RECV_F               0x0302  //@F00301 BGEEAAAA # TX_PDU_Recv

Sie definieren auch einige Funktionen in toolname.hpp:

#define LOG_LEVEL_ERROR 0
#define LOG_LEVEL_WARN 1
// ...

void init(void);
void probe(id,payload);
// etc

Überall in Ihrem Code können Sie Folgendes verwenden:

toolname<LOG_LEVEL>::log(EVENT_NAME,VALUE);

Das probe Funktion verwendet einige Montagelinien, um den Zeitstempel so schnell wie möglich abzurufen, und legt dann einen Eintrag im Puffer fest. Wir haben auch ein atomares Inkrement, um sicher einen Index zu finden, in dem das Protokollereignis gespeichert werden kann. Natürlich ist der Puffer kreisförmig.

Ich hoffe, die Idee wird nicht durch das Fehlen von Beispielcode verschleiert.


1

Eigentlich ein bisschen überrascht, dass nicht viele über Google / Benchmark gesprochen haben , während es etwas umständlich ist, den spezifischen Codebereich zu pinnen, insbesondere wenn die Codebasis ein wenig groß ist, aber ich fand dies wirklich hilfreich, wenn es in Kombination mit verwendet wirdcallgrind

IMHO ist hier der Schlüssel zu identifizieren, der den Engpass verursacht. Ich würde jedoch versuchen, zuerst die folgenden Fragen zu beantworten und darauf basierend ein Tool auszuwählen

  1. Ist mein Algorithmus korrekt?
  2. Gibt es Schlösser, die sich als Engpässe erweisen?
  3. Gibt es einen bestimmten Codeabschnitt, der sich als Schuldiger herausstellt?
  4. Wie wäre es mit IO, gehandhabt und optimiert?

valgrind mit der Kombination von callrind und kcachegrindsollte eine anständige Schätzung der oben genannten Punkte geliefert werden. Sobald festgestellt wurde, dass es Probleme mit einem Codeabschnitt gibt, würde ich vorschlagen, dass eine Mikro-Benchmark google benchmarkein guter Ausgangspunkt ist.


1

Verwenden Sie -pgflag beim Kompilieren und Verknüpfen des Codes und führen Sie die ausführbare Datei aus. Während dieses Programms ausgeführt wird, werden Profildaten in der Datei a.out gesammelt.
Es gibt zwei verschiedene Arten der Profilerstellung

1- Flat Profiling:
Durch Ausführen des Befehls erhielten gprog --flat-profile a.outSie die folgenden Daten
:
Wie viel Prozent der Gesamtzeit wurden für die Funktion aufgewendet, - wie viele Sekunden wurden in einer Funktion verbracht - einschließlich und ohne Aufrufe von Unterfunktionen,
- die Anzahl der Anrufe,
- die durchschnittliche Zeit pro Anruf.

2- Grafikprofilierung
des Befehls gprof --graph a.outzum Abrufen der folgenden Daten für jede Funktion, einschließlich
- In jedem Abschnitt ist eine Funktion mit einer Indexnummer gekennzeichnet.
- Über der Funktion befindet sich eine Liste von Funktionen, die die Funktion aufrufen.
- Unterhalb der Funktion befindet sich eine Liste der Funktionen, die von der Funktion aufgerufen werden.

Weitere Informationen finden Sie unter https://sourceware.org/binutils/docs-2.32/gprof/.


0

Da niemand Arm MAP erwähnte, würde ich es hinzufügen, da ich Map erfolgreich verwendet habe, um ein wissenschaftliches C ++ - Programm zu profilieren.

Arm MAP ist der Profiler für parallele, Multithread- oder Single-Threaded-C-, C ++ -, Fortran- und F90-Codes. Es bietet detaillierte Analysen und Engpässe, die auf die Quelllinie hinweisen. Im Gegensatz zu den meisten Profilern ist es so konzipiert, dass pthreads, OpenMP oder MPI für parallelen Code und Thread-Code profiliert werden können.

MAP ist kommerzielle Software.


0

Verwenden Sie eine Debugging-Software, um festzustellen, wo der Code langsam ausgeführt wird.

Denken Sie nur, Sie haben ein Hindernis, während Sie in Bewegung sind, dann verringert es Ihre Geschwindigkeit

So verbrauchen die Schleifen unerwünschter Neuzuweisungen, Pufferüberläufe, Suchen, Speicherverluste usw. mehr Ausführungsleistung, was sich nachteilig auf die Leistung des Codes auswirkt. Stellen Sie sicher, dass Sie der Kompilierung -pg hinzufügen, bevor Sie ein Profil erstellen:

g++ your_prg.cpp -pgoder cc my_program.cpp -g -pggemäß Ihrem Compiler

Ich habe es noch nicht ausprobiert, aber ich habe gute Dinge über Google-Perftools gehört. Es ist definitiv einen Versuch wert.

valgrind --tool=callgrind ./(Your binary)

Es wird eine Datei mit dem Namen gmon.out oder callgrind.out.x generiert. Sie können diese Datei dann mit kcachegrind oder dem Debugger-Tool lesen. Sie erhalten eine grafische Analyse der Dinge mit Ergebnissen, z. B. welche Linien wie viel kosten.

Ich glaube schon

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.