Der Originalartikel kann hier gelesen werden .
Hallo! Als Aperitif für den Ruby-Profiler wollte ich darüber sprechen, wie vorhandene Ruby- und Python-Profiler funktionieren. Es wird auch helfen, die Frage zu beantworten, die mir viele Leute stellen: "Wie schreibe ich einen Profiler?"
In diesem Artikel konzentrieren wir uns auf Prozessorprofiler (und nicht beispielsweise auf Speicher- / Heap-Profiler). Ich werde einige grundlegende Ansätze zum Schreiben eines Profilers behandeln, Codebeispiele bereitstellen und viele Beispiele für beliebte Profiler in Ruby und Python zeigen sowie Ihnen zeigen, wie sie unter der Haube funktionieren.
Möglicherweise enthält der Artikel Fehler (in Vorbereitung auf das Schreiben habe ich den Code von 14 Bibliotheken zur Profilerstellung teilweise durchgesehen und war mit vielen von ihnen bis jetzt nicht vertraut). Lassen Sie mich daher bitte wissen, wenn Sie sie finden ...
2 Arten von Profilern
Es gibt zwei Haupttypen von Prozessorprofilern: Sampling- und Tracing- Profiler.
Tracing-Profiler zeichnen jeden Funktionsaufruf in Ihrem Programm auf und liefern letztendlich einen Bericht. Stichprobenprofiler verfolgen einen statistischen Ansatz. Sie schreiben den Stapel alle paar Millisekunden und erstellen anhand dieser Daten einen Bericht.
Der Hauptgrund für die Verwendung eines Stichprobenprofilers anstelle eines Ablaufverfolgungsprofilers liegt in seiner geringen Gewichtung. Sie machen 20 oder 200 Bilder pro Sekunde - es dauert nicht lange. Diese Profiler sind sehr effektiv, wenn Sie ein ernstes Leistungsproblem haben (80% der Zeit wird damit verbracht, eine langsame Funktion aufzurufen), da 200 Schnappschüsse pro Sekunde ausreichen, um die Problemfunktion zu identifizieren!
Profiler
Als nächstes werde ich eine allgemeine Zusammenfassung der in diesem Artikel diskutierten Profiler bereitstellen ( von hier aus ). Ich werde die in diesem Artikel verwendeten Begriffe ( setitimer , rb_add_event_hook , ptrace ) etwas später erläutern . Interessanterweise werden alle Profiler mit einem kleinen Satz grundlegender Funktionen implementiert.
Python-Profiler
"Gdb-Hacks" ist eigentlich kein Python-Profiler - es verweist auf eine Website, auf der erklärt wird, wie ein Hacker-Profiler als Shell-Skript-Wrapper um gdb implementiert wird . Hier geht es speziell um Python, da neuere Versionen von gbd den Python-Stack tatsächlich für Sie bereitstellen. So etwas wie Pyflame für die Armen.
Ruby-Profiler
Fast alle diese Profiler befinden sich in Ihrem Prozess.
Bevor wir uns mit den Details dieser Profiler befassen , gibt es eine sehr wichtige Sache: Alle diese Profiler mit Ausnahme von Pyflame werden in Ihrem Python / Ruby-Prozess ausgeführt. Wenn Sie sich in einem Python / Ruby-Programm befinden, haben Sie normalerweise einfachen Zugriff auf den Stapel. Hier ist beispielsweise ein einfaches Python-Programm, das den Inhalt des Stapels jedes laufenden Threads druckt:
import sys
import traceback
def bar():
foo()
def foo():
for _, frame in sys._current_frames().items():
for line in traceback.extract_stack(frame):
print line
bar()
Hier ist die Konsolenausgabe. Sie können sehen, dass es Funktionsnamen aus dem Stapel, Zeilennummern und Dateinamen enthält - was auch immer Sie für die Profilerstellung benötigen.
('test2.py', 12, '<module>', 'bar()')
('test2.py', 5, 'bar', 'foo()')
('test2.py', 9, 'foo', 'for line in traceback.extract_stack(frame):')
In Ruby ist es noch einfacher: Sie können den Puts-Aufrufer verwenden , um den Stapel abzurufen.
Die meisten dieser Profiler sind Leistungserweiterungen von C, daher unterscheiden sie sich geringfügig. Solche Erweiterungen für Ruby / Python-Programme haben jedoch auch einfachen Zugriff auf den Aufrufstapel.
So funktionieren Tracing-Profiler
Ich habe alle Ruby- und Python- Ablaufverfolgungsprofile in den obigen Tabellen aufgelistet : rblineprof, ruby-prof, line_profiler und cProfile . Sie arbeiten alle auf ähnliche Weise. Sie zeichnen jeden Funktionsaufruf auf und sind C-Erweiterungen, um den Overhead zu reduzieren.
Wie arbeiten Sie? Sowohl in Ruby als auch in Python können Sie einen Rückruf angeben, der ausgelöst wird, wenn verschiedene Interpreterereignisse auftreten (z. B. "Funktionsaufruf" oder "Codezeilenausführung"). Wenn der Rückruf aufgerufen wird, schreibt er den Stapel für eine spätere Analyse.
Es ist hilfreich, genau zu sehen, wo sich diese Rückrufe im Code befinden, daher werde ich auf die relevanten Codezeilen auf github verweisen.
In Python können Sie den Rückruf mit
PyEval_SetTraceoder anpassen PyEval_SetProfile. Dies wird im Dokumentationsabschnitt beschriebenProfilerstellung und Ablaufverfolgung in Python. Es heißt " PyEval_SetTraceähnlich, PyEval_SetProfileaußer dass die Trace-Funktion Zeilennummernereignisse empfängt".
Der Code:
line_profilerrichtet seinen Rückruf ein mitPyEval_SetTrace: sieheline_profiler.pyxZeile 157cProfilerichtet seinen Rückruf ein mitPyEval_SetProfile: siehe_lsprof.cZeile 693 (cProfile wird mit lsprof implementiert )
In Ruby können Sie Ihren Rückruf mit anpassen
rb_add_event_hook. Ich konnte keine Dokumentation darüber finden, aber so sieht es aus.
rb_add_event_hook(prof_event_hook,
RUBY_EVENT_CALL | RUBY_EVENT_RETURN |
RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN |
RUBY_EVENT_LINE, self);
Unterschrift
prof_event_hook:
static void
prof_event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)
So etwas wie
PyEval_SetTracein Python, aber in einer flexibleren Form - Sie können auswählen, über welche Ereignisse Sie benachrichtigt werden möchten (z. B. "nur Funktionsaufrufe").
Der Code:
tracing-
Der Hauptnachteil der auf diese Weise implementierten Ablaufverfolgungsprofiler besteht darin, dass sie für jeden ausgeführten Funktions- / Zeilenaufruf eine feste Codemenge hinzufügen. Sie können die falschen Entscheidungen treffen! Wenn Sie beispielsweise zwei Implementierungen von etwas haben - eine mit vielen Funktionsaufrufen und die andere ohne, die normalerweise dieselbe Zeit in Anspruch nehmen, erscheint die erste mit vielen Funktionsaufrufen bei der Profilerstellung langsamer.
Zur Veranschaulichung habe ich eine kleine Datei mit dem Namen
test.pymit dem folgenden Inhalt und verglichen die Ausführungszeit python -mcProfile test.pyund python test.py. python. test.pyabgeschlossen in ca. 0,6 s und in python -mcProfile test.pyca. 1 s. Für dieses spezielle Beispiel habe ich cProfileeinen zusätzlichen Overhead von ~ 60% hinzugefügt.
Die DokumentationcProfileist geschrieben, die
Interpretation von Python erhöht den Laufzeitaufwand erheblich, sodass die deterministische Profilerstellung die Verarbeitung in herkömmlichen Anwendungen tendenziell etwas belastet
Dies scheint eine ziemlich vernünftige Aussage zu sein - das vorherige Beispiel (das 3,5 Millionen Funktionsaufrufe und nichts anderes ausführt) ist offensichtlich kein reguläres Python-Programm, und fast jedes andere Programm hat weniger Overhead.
Ich habe den
ruby-profRuby-Tracing-Profiler nicht überprüft , aber in seiner README-Datei steht Folgendes:
Die meisten Programme werden etwa halb so langsam ausgeführt, während hochrekursive Programme (wie der Fibonacci-Serientest) dreimal langsamer ausgeführt werden .
Funktionsweise von Sampling-Profilern: Setitimer
Zeit, über die zweite Art von Profiler zu sprechen: Sampling-Profiler!
Die meisten Sampling-Profiler in Ruby und Python werden mithilfe eines Systemaufrufs implementiert
setitimer. Was ist das?
Angenommen, Sie möchten 50 Mal pro Sekunde eine Momentaufnahme des Programmstapels erstellen. Dies kann wie folgt erfolgen:
- Bitten Sie den Linux-Kernel, Ihnen alle 20 Millisekunden ein Signal zu senden (mithilfe eines Systemaufrufs
setitimer). - Registrieren Sie einen Signalhandler für einen Stack-Snapshot, wenn ein Signal empfangen wird.
- Wenn die Profilerstellung abgeschlossen ist, bitten Sie Linux, die Signalisierung zu beenden und das Ergebnis bereitzustellen!
Wenn Sie einen praktischen Anwendungsfall
setitimerfür die Implementierung eines Stichprobenprofilers sehen möchten, ist das stacksampler.pybeste Beispiel meiner Meinung nach ein nützlicher, funktionierender Profiler, der in Python etwa 100 Zeilen umfasst. Und das ist sehr cool!
Der Grund, warum
stacksampler.pyin Python nur 100 Zeilen benötigt werden, ist, dass beim Registrieren einer Python-Funktion als Signalhandler die Funktion an den aktuellen Stapel Ihres Programms übergeben wird. Daher ist es stacksampler.pysehr einfach, einen Signalhandler zu registrieren:
def _sample(self, signum, frame):
stack = []
while frame is not None:
stack.append(self._format_frame(frame))
frame = frame.f_back
stack = ';'.join(reversed(stack))
self._stack_counts[stack] += 1
Es wird nur ein Stapel aus einem Frame entfernt und die Häufigkeit erhöht, mit der ein bestimmter Stapel angezeigt wurde. So einfach! So cool!
Werfen wir einen Blick auf alle anderen Profiler, die sie verwenden,
setitimerund finden heraus, wo in dem Code, den sie aufrufen setitimer:
stackprof(Ruby):stackprof.c118perftools.rb(Ruby): , , , , gem (?)stacksampler(Python):stacksampler.py51statprof(Python):statprof.py239vmprof(Python):vmprof_unix.c294
Das Wichtigste an
setitimer- Sie müssen entscheiden, wie die Zeit gezählt werden soll. Möchten Sie 20 ms in Echtzeit? 20ms Benutzer CPU-Zeit? 20ms Benutzer + System CPU Zeit? Wenn Sie sich die obigen Links genau ansehen, werden Sie feststellen, dass diese Profiler tatsächlich unterschiedliche Dinge verwenden setitimer- manchmal ist das Verhalten anpassbar und manchmal nicht. Die Manpage ist setitimerkurz und für alle möglichen Konfigurationen lesenswert. wies auf einen interessanten Anwendungsfall
@mgedminauf Twitter hinsetitimer . Dieses Problem und dieses Problem enthalten weitere Details.
Ein interessanter Nachteil von Profilern basierend auf
setitimer- Welche Timer lösen Signale aus! Signale unterbrechen manchmal Systemanrufe! Systemaufrufe dauern manchmal einige Millisekunden! Wenn Sie zu oft Schnappschüsse machen, können Sie das Programm dazu bringen, Systemaufrufe auf unbestimmte Zeit auszuführen!
Sampling-Profiler, die kein Setitimer verwenden
Es gibt mehrere Stichprobenprofiler, die nicht verwendet werden
setitimer:
pyinstrumentverwendetPyEval_SetProfile(es ist also eine Art Tracing-Profiler), sammelt jedoch nicht immer Stapel-Snapshots, wenn der Trace-Rückruf aufgerufen wird. Hier ist der Code, der das Timing des Stack-Trace- Snapshots festlegt . Weitere Informationen zu dieser Lösung finden Sie in diesem Blog . (Grundsätzlich:setitimerErmöglicht das Profilieren nur des Hauptthreads in Python.)pyflameProfiliert Python-Code außerhalb eines Prozesses mithilfe eines Systemaufrufsptrace. Er benutzt eine Schleife, in der er Fotos macht, eine gewisse Zeit schläft und dasselbe noch einmal macht. Hier ist ein Anruf zum Warten.python-flamegraphVerfolgt einen ähnlichen Ansatz, bei dem ein neuer Thread in Ihrem Python-Prozess gestartet wird und die Stack-Traces, Sleeps und Loops erneut abgerufen werden. Hier ist ein Anruf zum Warten .
Alle drei dieser Profiler machen Echtzeit-Schnappschüsse.
Pyflame-Blog-Beiträge
Ich habe fast die ganze Zeit in diesem Beitrag mit anderen Profilern als verbracht
pyflame, aber es interessiert mich am meisten, weil es Ihr Python-Programm aus einem separaten Prozess heraus profiliert, weshalb ich möchte, dass mein Ruby-Profiler ähnlich funktioniert.
Natürlich ist alles etwas komplizierter als ich beschrieben habe. Ich werde nicht auf Details eingehen, aber Evan Klitzke hat in seinem Blog viele gute Artikel darüber geschrieben:
- Pyflame: Der Ptracing Profiler von Uber Engineering für Python - Eine Einführung in Pyflame.
- Pyflame Dual Interpreter Mode - Wie es gleichzeitig Python 2 und Python 3 unterstützt
- Eine unerwartete Python-ABI-Änderung - Informationen zum Hinzufügen von Unterstützung für Python 3.6;
- Dumping von Python-Stacks mit mehreren Threads ;
- Pyflame-Pakete ;
ptraceund Systemaufrufe in Python;- Verwendung von ptrace für Spaß und Gewinn, ptrace (Fortsetzung) ;
Weitere Informationen finden Sie unter eklitzke.org . Dies sind alles sehr interessante Dinge, über die ich genauer lesen werde - vielleicht wird es
ptracesich als besser herausstellen als process_vm_readvfür die Implementierung eines Ruby-Profilers! Es hat process_vm_readvweniger Overhead, da es den Prozess nicht stoppt, aber es kann Ihnen auch einen falschen Schnappschuss geben, da es den Prozess nicht stoppt :). In meinen Experimenten war es kein großes Problem, widersprüchliche Bilder zu erhalten, aber ich denke, dass ich hier eine Reihe von Experimenten durchführen werde.
Das ist alles für heute!
Es gibt viele wichtige Feinheiten, auf die ich in diesem Beitrag nicht eingegangen bin - zum Beispiel habe ich das im Grunde gesagt
vmprofund stacksampler- sind ähnlich (sie sind nicht -vmprofunterstützt die Erstellung von Zeichenfolgenprofilen und die Erstellung von Profilen für in C geschriebene Python-Funktionen, was den Profiler meiner Meinung nach komplexer macht. Aber sie haben einige der gleichen Grundprinzipien, und deshalb denke ich, dass der heutige Rückblick ein guter Ausgangspunkt sein wird.
TDD mit und ohne Pytest. Kostenloses Webinar