Enthält Profiling-Programme in C ++


Manchmal kann es erforderlich sein, die Programmleistung oder den Speicherverbrauch in einem C ++ - Programm zu profilieren. Leider ist dies oft nicht so einfach, wie es scheint.

Hier werden die Funktionen von Profiling-Programmen mit den Tools valgrind und google perftools erläutert . Das Material erwies sich als nicht sehr strukturiert, es ist eher ein Versuch, eine Wissensbasis „für persönliche Zwecke“ aufzubauen, damit Sie sich in Zukunft nicht verzweifelt daran erinnern müssen, „warum dies nicht funktioniert“ oder „wie es geht“. Höchstwahrscheinlich sind hier nicht alle nicht offensichtlichen Fälle betroffen. Wenn Sie etwas hinzuzufügen haben, schreiben Sie dies bitte in die Kommentare.

Alle Beispiele laufen auf dem Linux-System.

Laufzeit-Profilerstellung


Vorbereitung


Um die Funktionen der Profilerstellung zu analysieren, führe ich kleine Programme aus, die normalerweise aus einer main.cpp-Datei und einer func.cpp-Datei zusammen mit der Aufnahme bestehen.
Ich werde sie mit dem G ++ 8.3.0 Compiler kompilieren .

Da das Erstellen von Profilen für nicht optimierte Programme eine ziemlich seltsame Aufgabe ist, kompilieren wir mit der Option -Ofast. Um Debug-Zeichen in der Ausgabe zu erhalten, vergessen wir nicht, die Option -g hinzuzufügen. Manchmal werden jedoch anstelle von normalen Funktionsnamen nur nicht hörbare Anrufadressen angezeigt. Dies bedeutet, dass eine "Randomisierung der Adressraumzuweisung" stattgefunden hat. Dies kann durch Aufrufen des Befehls nm in der Binärdatei ermittelt werden. Wenn die meisten Adressen so aussehen wie 00000000000030e0 (eine große Anzahl von Nullen am Anfang), ist dies höchstwahrscheinlich der Fall. In einem normalen Programm sehen die Adressen wie folgt aus: 0000000000402fa0. Daher müssen Sie die Option -no-pie hinzufügen. Infolgedessen sieht der vollständige Optionssatz folgendermaßen aus:
-Ofast -g -no-pie

Um die Ergebnisse anzuzeigen, verwenden wir das Programm KCachegrind , das mit dem Callgrind- Berichtsformat arbeiten kann

Callgrind


Das erste Dienstprogramm, das wir uns heute ansehen werden, ist callgrind . Dieses Dienstprogramm ist Teil des valgrind-Tools. Es emuliert jede ausführbare Anweisung des Programms und gibt auf der Grundlage interner Metriken zu den „Kosten“ jeder Anweisung die Schlussfolgerung aus, die wir benötigen. Aufgrund dieser Vorgehensweise kann es vorkommen, dass Callgrind den nächsten Befehl nicht erkennt und mit einem Fehler ausfällt
Nicht erkannte Anweisung an Adresse
Der einzige Ausweg aus dieser Situation ist, alle Kompilierungsoptionen zu überdenken und zu versuchen, die Störung zu finden

Erstellen wir ein Programm zum Testen dieses Tools, das aus einer gemeinsam genutzten Bibliothek und einer statischen Bibliothek besteht (Bibliotheken werden später in anderen Tests aufgegeben). Jede Bibliothek sowie das Programm selbst bieten eine einfache Rechenfunktion, beispielsweise die Berechnung der Fibonacci-Sequenz.

static_lib
////////////////// // static_lib.h // ////////////////// #ifndef SERVER_STATIC_LIB_H #define SERVER_STATIC_LIB_H int func_static_lib(int arg); #endif //SERVER_STATIC_LIB_H //////////////////// // static_lib.cpp // /////////////////// #include "static_lib.h" #include "static_func.h" #include <cstddef> int func_static_lib(int arg) { return static_func(arg); } /////////////////// // static_func.h // /////////////////// #ifndef TEST_PROFILER_STATIC_FUNC_H #define TEST_PROFILER_STATIC_FUNC_H int static_func(int arg); #endif //TEST_PROFILER_STATIC_FUNC_H ///////////////////// // static_func.cpp // ///////////////////// #include "static_func.h" int static_func(int arg) { int fst = 0; int snd = 1; for (int i = 0; i < arg; i++) { int tmp = (fst + snd) % 17769897; fst = snd; snd = tmp; } return fst; } 


shared_lib
 ////////////////// // shared_lib.h // ////////////////// #ifndef TEST_PROFILER_SHARED_LIB_H #define TEST_PROFILER_SHARED_LIB_H int func_shared_lib(int arg); #endif //TEST_PROFILER_SHARED_LIB_H //////////////////// // shared_lib.cpp // //////////////////// #include "shared_lib.h" #include "shared_func.h" int func_shared_lib(int arg) { return shared_func(arg); } /////////////////// // shared_func.h // /////////////////// #ifndef TEST_PROFILER_SHARED_FUNC_H #define TEST_PROFILER_SHARED_FUNC_H int shared_func(int arg); #endif //TEST_PROFILER_SHARED_FUNC_H ///////////////////// // shared_func.cpp // ///////////////////// #include "shared_func.h" int shared_func(int arg) { int result = 1; for (int i = 1; i < arg; i++) { result = (int)(((long long)result * i) % 19637856977); } return result; } 


main
 ////////////// // main.cpp // ////////////// #include <iostream> #include "static_lib.h" #include "shared_lib.h" #include "func.h" int main(int argc, char **argv) { if (argc != 2) { std::cout << "Incorrect args"; return -1; } const int arg = std::atoi(argv[1]); std::cout << "result: " << func_static_lib(arg) << " " << func_shared_lib(arg) << " " << func(arg); return 0; } //////////// // func.h // //////////// #ifndef TEST_PROFILER_FUNC_H #define TEST_PROFILER_FUNC_H int func(int arg); #endif //TEST_PROFILER_FUNC_H ////////////// // func.cpp // ////////////// #include "func.h" int func(int arg) { int fst = 1; int snd = 1; for (int i = 0; i < arg; i++) { int res = (fst + snd + 1) % 19845689; fst = snd; snd = res; } return fst; } 


Wir kompilieren das Programm und führen valgrind folgendermaßen aus:

 valgrind --tool=callgrind ./test_profiler 100000000 


Wir sehen, dass für eine statische Bibliothek und eine reguläre Funktion das Ergebnis dem erwarteten ähnlich ist. In der dynamischen Bibliothek konnte callgrind die Funktion jedoch nicht vollständig auflösen.

Um dies zu beheben, müssen Sie beim Starten des Programms die Variable LD_BIND_NOW wie folgt auf 1 setzen:

 LD_BIND_NOW=1 valgrind --tool=callgrind ./test_profiler 100000000 


Und jetzt ist, wie Sie sehen, alles in Ordnung

Das nächste Callgrind-Problem bei der Profilerstellung durch Emulation von Anweisungen besteht darin, dass die Programmausführung erheblich verlangsamt wird. Dies kann eine falsche relative Schätzung der Ausführungszeit verschiedener Teile des Codes enthalten.

Schauen wir uns diesen Code an:

 int func(int arg) { int fst = 1; int snd = 1; std::ofstream file("tmp.txt"); for (int i = 0; i < arg; i++) { int res = (fst + snd + 1) % 19845689; std::string r = std::to_string(res); file << res; file.flush(); fst = snd; snd = res + r.size(); } return fst; } 

Hier habe ich für jede Iteration der Schleife eine kleine Datenmenge zu einer Datei hinzugefügt. Da das Schreiben in eine Datei als Gegengewicht ziemlich langwierig ist, habe ich zu jeder Iteration der Schleife eine Zeilengenerierung aus einer Zahl hinzugefügt. Offensichtlich dauert in diesem Fall der Schreibvorgang in die Datei länger als der Rest der Logik der Funktion. Aber callgrind denkt anders:


Es ist auch zu bedenken, dass Callgrind die Kosten einer Funktion nur messen kann, wenn sie funktioniert. Die Funktion funktioniert nicht - daher steigen die Kosten nicht. Dies erschwert das Debuggen von Programmen, die von Zeit zu Zeit in die Sperre eintreten oder mit einem blockierenden Dateisystem / Netzwerk arbeiten. Lassen Sie uns überprüfen:

 #include "func.h" #include <mutex> static std::mutex mutex; int funcImpl(int arg) { std::lock_guard<std::mutex> lock(mutex); int fst = 1; int snd = 1; for (int i = 0; i < arg; i++) { int res = (fst + snd + 1) % 19845689; fst = snd; snd = res; } return fst; } int func2(int arg){ return funcImpl(arg); } int func(int arg) { return funcImpl(arg); } int main(int argc, char **argv) { if (argc != 2) { std::cout << "Incorrect args"; return -1; } const int arg = std::atoi(argv[1]); auto future = std::async(std::launch::async, &func2, arg); std::cout << "result: " << func(arg) << std::endl; std::cout << "second result " << future.get() << std::endl; return 0; } 

Hier haben wir die gesamte Funktionsausführung in die Sperre des Mutex eingeschlossen und diese Funktion aus zwei verschiedenen Threads aufgerufen. Das Ergebnis von Callgrind ist ziemlich vorhersehbar - er sieht kein Problem beim Erfassen eines Mutex:


Daher haben wir einige Probleme bei der Verwendung des Callgrind-Profilers untersucht. Fahren wir mit dem nächsten Testthema fort - dem Google Perftools Profiler

Google Perftools


Im Gegensatz zu Callgrind arbeitet der Google Profiler nach einem anderen Prinzip.
Anstatt jeden Befehl des ausführbaren Programms zu analysieren, unterbricht es das Programm in regelmäßigen Abständen und versucht festzustellen, in welcher Funktion es sich gerade befindet. Infolgedessen wirkt sich dies fast nicht auf die Leistung der ausgeführten Anwendung aus. Dieser Ansatz hat aber auch seine Schwächen.

Beginnen wir mit der Profilerstellung des ersten Programms mit zwei Bibliotheken.

In der Regel müssen Sie zum Starten der Profilerstellung mit diesem Dienstprogramm die Bibliothek libprofiler.so vorab laden, die Abtastfrequenz festlegen und die Datei angeben, in der der Speicherauszug gespeichert werden soll. Leider muss der Profiler das Programm "von selbst" beenden. Bei erzwungener Beendigung des Programms wird der Bericht einfach nicht gelöscht. Dies ist unpraktisch, wenn Sie ein Profil für langlebige Programme erstellen, die selbst nicht angehalten werden, z. B. Daemons. Um dieses Hindernis zu umgehen, habe ich folgendes Skript erstellt:

gprof.sh
 rnd=$RANDOM if [ $# -eq 0 ] then echo "./gprof.sh command args" echo "Run with variable N_STOP=true if hand stop required" exit fi libprofiler=$( dirname "${BASH_SOURCE[0]}" ) arg=$1 nostop=$N_STOP profileName=callgrind.out.$rnd.g gperftoolProfile=./gperftool."$rnd".txt touch $profileName echo "Profile name $profileName" if [[ $nostop = "true" ]] then echo "without stop" trap 'echo trap && kill -12 $PID && sleep 1 && kill -TERM $PID' TERM INT else trap 'echo trap && kill -TERM $PID' TERM INT fi if [[ $nostop = "true" ]] then CPUPROFILESIGNAL=12 CPUPROFILE_FREQUENCY=1000000 CPUPROFILE=$gperftoolProfile LD_PRELOAD=${libprofiler}/libprofiler.so "${@:1}" & else CPUPROFILE_FREQUENCY=1000000 CPUPROFILE=$gperftoolProfile LD_PRELOAD=${libprofiler}/libprofiler.so "${@:1}" & fi PID=$! if [[ $nostop = "true" ]] then sleep 1 kill -12 $PID fi wait $PID trap - TERM INT wait $PID EXIT_STATUS=$? echo $PWD ${libprofiler}/pprof --callgrind $arg $gperftoolProfile* > $profileName echo "Profile name $profileName" rm -f $gperftoolProfile* 


Dieses Dienstprogramm muss ausgeführt werden und als Parameter den Namen der ausführbaren Datei und eine Liste ihrer Parameter übergeben. Außerdem wird davon ausgegangen, dass sich neben dem Skript die Dateien befinden, die libprofiler.so und pprof benötigen. Wenn das Programm langlebig ist und die Ausführung unterbricht, müssen Sie die Variable N_STOP auf true setzen. Beispiel:
 N_STOP=true ./gprof.sh ./test_profiler 10000000000 

Am Ende der Arbeit generiert das Skript einen Bericht in meinem bevorzugten Callgrind-Format.

Lassen Sie uns also unser Programm unter diesem Profiler ausführen.
 ./gprof.sh ./test_profiler 1000000000 


Im Prinzip ist alles ziemlich klar.

Wie gesagt, der Google Profiler beendet die Ausführung des Programms und berechnet die aktuelle Funktion. Wie macht er das? Dazu dreht er den Stapel auf. Aber was ist, wenn das Programm zum Zeitpunkt der Stapelaktion den Stapel selbst abwickelt? Nun, offensichtlich wird nichts Gutes passieren. Lass es uns ausprobieren. Schreiben wir eine solche Funktion:

 int runExcept(int res) { if (res % 13 == 0) { throw std::string("Exception"); } return res; } int func(int arg) { int fst = 1; int snd = 1; for (int i = 0; i < arg; i++) { int res = (fst + snd + 1) % 19845689; try { res = runExcept(res); } catch (const std::string &e) { res = res - 1; } fst = snd; snd = res; } return fst; } 

Führen Sie die Profilerstellung aus. Das Programm friert ziemlich schnell ein.

Es gibt ein weiteres Problem im Zusammenhang mit der Besonderheit der Profiler-Operation. Angenommen, wir haben es geschafft, den Stapel aufzuheben, und jetzt müssen wir die Adressen mit den spezifischen Funktionen des Programms abgleichen. Dies kann sehr untrivial sein, da in C ++ eine ziemlich große Anzahl von Funktionen inline sind. Schauen wir uns ein Beispiel an:

 #include "func.h" static int func1(int arg) { std::cout << 1 << std::endl; return func(arg); } static int func2(int arg) { std::cout << 2 << std::endl; return func(arg); } static int func3(int arg) { std::cout << 3 << std::endl; if (arg % 2 == 0) { return func2(arg); } else { return func1(arg); } } int main(int argc, char **argv) { if (argc != 2) { std::cout << "Incorrect args"; return -1; } const int arg = std::atoi(argv[1]); int arg2 = func3(arg); int arg3 = func(arg); std::cout << "result: " << arg2 + arg3; return 0; } 

Wenn Sie das Programm beispielsweise folgendermaßen ausführen:
 ./gprof.sh ./test_profiler 1000000000 

dann wird die Funktion func1 niemals aufgerufen. Aber der Profiler denkt anders:

(Übrigens hat sich valgrind hier entschlossen, bescheiden zu schweigen und nicht anzugeben, von welcher bestimmten Funktion der Aufruf kam).

Speicherprofilerstellung


Oft gibt es Situationen, in denen der Speicher von der Anwendung irgendwo "fließt". Wenn dies auf eine fehlende Ressourcenbereinigung zurückzuführen ist, sollte Memcheck helfen, das Problem zu identifizieren. In modernem C ++ ist es jedoch nicht so schwierig, auf manuelles Ressourcenmanagement zu verzichten. unique_ptr, shared_ptr, vector, map machen Bare-Point-Manipulation sinnlos.

In solchen Anwendungen tritt jedoch ein Speicherverlust auf. Wie läuft das Ganz einfach, es ist in der Regel so etwas wie "den Wert in eine langlebige Karte einfügen, aber vergessen, sie zu löschen". Lassen Sie uns versuchen, diese Situation zu verfolgen.

Dazu schreiben wir unsere Testfunktion auf diese Weise um

 #include "func.h" #include <deque> #include <string> #include <map> static std::deque<std::string> deque; static std::map<int, std::string> map; int func(int arg) { int fst = 1; int snd = 1; for (int i = 0; i < arg; i++) { int res = (fst + snd + 1) % 19845689; fst = snd; snd = res; deque.emplace_back(std::to_string(res) + " integer"); map[i] = "integer " + std::to_string(res); deque.pop_front(); if (res % 200 != 0) { map.erase(i - 1); } } return fst; } 

Hier fügen wir bei jeder Iteration einige Elemente zur Karte hinzu, und versehentlich (wahr, wahr) vergessen wir, sie manchmal von dort zu entfernen. Um unsere Augen abzuwenden, foltern wir außerdem std :: deque ein bisschen.

Wir werden Speicherlecks mit zwei Tools beheben - Valgrind Massif und Google Heapdump .

Massif


Führen Sie das Programm mit diesem Befehl aus
 valgrind --tool=massif ./test_profiler 1000000 

Und wir sehen so etwas wie

Massif
 time=1277949333 mem_heap_B=313518 mem_heap_extra_B=58266 mem_stacks_B=0 heap_tree=detailed n4: 313518 (heap allocation functions) malloc/new/new[], --alloc-fns, etc. n1: 195696 0x109A69: func(int) (new_allocator.h:111) n0: 195696 0x10947A: main (main.cpp:18) n1: 72704 0x52BA414: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25) n1: 72704 0x4010731: _dl_init (dl-init.c:72) n1: 72704 0x40010C8: ??? (in /lib/x86_64-linux-gnu/ld-2.27.so) n1: 72704 0x0: ??? n1: 72704 0x1FFF0000D1: ??? n0: 72704 0x1FFF0000E1: ??? n2: 42966 0x10A7EC: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_mutate(unsigned long, unsigned long, char const*, unsigned long) (new_allocator.h:111) n1: 42966 0x10AAD9: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long) (basic_string.tcc:466) n1: 42966 0x1099D4: func(int) (basic_string.h:1932) n0: 42966 0x10947A: main (main.cpp:18) n0: 0 in 2 places, all below massif's threshold (1.00%) n0: 2152 in 10 places, all below massif's threshold (1.00%) 


Es ist zu erkennen, dass das Massiv eine Undichtigkeit in der Funktion feststellen konnte, es ist jedoch noch nicht klar, wo. Lassen Sie uns das Programm mit dem Flag -fno-inline neu erstellen und die Analyse erneut ausführen

Massiv
 time=3160199549 mem_heap_B=345142 mem_heap_extra_B=65986 mem_stacks_B=0 heap_tree=detailed n4: 345142 (heap allocation functions) malloc/new/new[], --alloc-fns, etc. n1: 221616 0x10CDBC: std::_Rb_tree_node<std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >* std::_Rb_tree<int, std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::_Select1st<std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >, std::less<int>, std::allocator<std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >::_M_create_node<std::piecewise_construct_t const&, std::tuple<int const&>, std::tuple<> >(std::piecewise_construct_t const&, std::tuple<int const&>&&, std::tuple<>&&) [clone .isra.81] (stl_tree.h:653) n1: 221616 0x10CE0C: std::_Rb_tree_iterator<std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > std::_Rb_tree<int, std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::_Select1st<std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >, std::less<int>, std::allocator<std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >::_M_emplace_hint_unique<std::piecewise_construct_t const&, std::tuple<int const&>, std::tuple<> >(std::_Rb_tree_const_iterator<std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >, std::piecewise_construct_t const&, std::tuple<int const&>&&, std::tuple<>&&) [clone .constprop.87] (stl_tree.h:2414) n1: 221616 0x10CF2B: std::map<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::less<int>, std::allocator<std::pair<int const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >::operator[](int const&) (stl_map.h:499) n1: 221616 0x10A7F5: func(int) (func.cpp:20) n0: 221616 0x109F8E: main (main.cpp:18) n1: 72704 0x52BA414: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25) n1: 72704 0x4010731: _dl_init (dl-init.c:72) n1: 72704 0x40010C8: ??? (in /lib/x86_64-linux-gnu/ld-2.27.so) n1: 72704 0x0: ??? n1: 72704 0x1FFF0000D1: ??? n0: 72704 0x1FFF0000E1: ??? n2: 48670 0x10B866: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_mutate(unsigned long, unsigned long, char const*, unsigned long) (basic_string.tcc:317) n1: 48639 0x10BB2C: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long) (basic_string.tcc:466) n1: 48639 0x10A643: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > std::operator+<char, std::char_traits<char>, std::allocator<char> >(char const*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&) [clone .constprop.86] (basic_string.h:6018) n1: 48639 0x10A7E5: func(int) (func.cpp:20) n0: 48639 0x109F8E: main (main.cpp:18) n0: 31 in 1 place, below massif's threshold (1.00%) n0: 2152 in 10 places, all below massif's threshold (1.00%) 


Jetzt ist klar, wo sich das Leck beim Hinzufügen des Kartenelements befindet. Das Massif kann kurzlebige Objekte erkennen, sodass Manipulationen mit std :: deque in diesem Dump nicht sichtbar sind.

Heapdump


Damit Google Heapdump funktioniert, müssen Sie die tcmalloc- Bibliothek verknüpfen oder vorab laden . Diese Bibliothek ersetzt die Standardfunktionen der Speicherzuweisung malloc, free, ... Sie kann auch Informationen über die Verwendung dieser Funktionen sammeln, die wir bei der Analyse des Programms verwenden werden.

Da diese Methode sehr langsam arbeitet (auch im Vergleich zum Massiv), empfehle ich, die Kompilierung von Funktionen beim Kompilieren sofort mit der Option -fno-inline zu deaktivieren. Also bauen wir unsere Anwendung neu auf und laufen mit dem Team
 HEAPPROFILESIGNAL=23 HEAPPROFILE=./heap ./test_profiler 100000000 

Hier wird davon ausgegangen, dass die tcmalloc-Bibliothek mit unserer Anwendung verknüpft ist.

Nun warten wir einige Zeit, bis sich ein merkliches Leck gebildet hat, und senden unserem Prozess ein Signal 23
 kill -23 <pid> 

Als Ergebnis wird eine Datei mit dem Namen heap.0001.heap angezeigt, die wir mit dem Befehl in das Callgrind-Format konvertieren

 pprof ./test_profiler "./heap.0001.heap" --inuse_space --callgrind > callgrind.out.4 

Beachten Sie auch die pprof-Optionen. Sie können zwischen den Optionen inuse_space , inuse_objects , alloc_space und alloc_objects wählen , die den verwendeten Speicherplatz bzw. die verwendeten Objekte oder den für die gesamte Programmdauer zugewiesenen Speicherplatz bzw. die zugewiesenen Objekte anzeigen . Wir sind an der Option inuse_space interessiert, die den aktuell verwendeten Speicherplatz anzeigt.

Öffnen Sie unser Lieblings-kCacheGrind und sehen Sie

std :: map hat zu viel Speicher verbraucht. Wahrscheinlich eine Undichtigkeit.

Schlussfolgerungen


Das Profiling in C ++ ist eine sehr schwierige Aufgabe. Hier haben wir es mit Inlining-Funktionen, nicht unterstützten Anweisungen, falschen Ergebnissen usw. zu tun. Es ist nicht immer möglich, den Ergebnissen des Profilers zu vertrauen.

Zusätzlich zu den oben vorgeschlagenen Funktionen gibt es andere Tools für die Profilerstellung - Perf, Intel VTune und andere. Sie zeigen aber auch einige dieser Mängel. Vergessen Sie daher nicht die "Grandfather" -Profilierungsmethode, bei der die Ausführungszeit von Funktionen gemessen und im Protokoll angezeigt wird.

Wenn Sie interessante Techniken zur Profilerstellung für Ihren Code haben, veröffentlichen Sie diese bitte in den Kommentaren

Source: https://habr.com/ru/post/de482040/


All Articles