Fitur program pembuatan profil dalam C ++


Kadang-kadang, mungkin perlu untuk profil kinerja program atau konsumsi memori dalam program C ++. Sayangnya, seringkali ini tidak semudah kelihatannya.

Di sini kita akan mempertimbangkan fitur program profil menggunakan alat valgrind dan google perftools . Materi itu ternyata tidak terlalu terstruktur, itu lebih merupakan upaya untuk mengumpulkan basis pengetahuan "untuk tujuan pribadi" sehingga di masa depan Anda tidak perlu dengan panik mengingat "mengapa ini tidak berhasil" atau "bagaimana melakukannya". Kemungkinan besar, tidak semua kasus yang tidak jelas akan terpengaruh di sini, jika Anda memiliki sesuatu untuk ditambahkan, silakan tulis di komentar.

Semua contoh akan berjalan di sistem linux.

Runtime Profiling


Persiapan


Untuk menganalisis fitur-fitur pembuatan profil, saya akan menjalankan program kecil, biasanya terdiri dari satu file main.cpp dan satu file func.cpp beserta inklusi.
Saya akan mengkompilasinya dengan kompiler g ++ 8.3.0 .

Karena profiling program yang tidak dioptimalkan adalah tugas yang agak aneh, kami akan mengkompilasi dengan opsi -Ofast , dan untuk mendapatkan karakter debug dalam output, kami tidak akan lupa untuk menambahkan opsi -g . Namun, terkadang alih-alih nama fungsi normal, Anda hanya dapat melihat alamat panggilan yang tidak terdengar. Ini berarti bahwa telah ada "pengacakan alokasi ruang alamat". Ini dapat ditentukan dengan memanggil perintah nm pada biner. Jika sebagian besar alamat terlihat seperti ini 0000000000003030e0 (sejumlah besar nol di awal), maka kemungkinan besar ini dia. Dalam program normal, alamatnya terlihat seperti 0000000000402fa0. Karena itu, Anda perlu menambahkan opsi -no-pie . Akibatnya, set lengkap opsi akan terlihat seperti ini:
-Bergas -g -tidak-pai

Untuk melihat hasilnya, kami akan menggunakan program KCachegrind , yang dapat bekerja dengan format laporan callgrind

Callgrind


Utilitas pertama yang akan kita lihat hari ini adalah callgrind . Utilitas ini adalah bagian dari alat valgrind. Ini mengemulasi setiap instruksi yang dapat dieksekusi dari program dan, berdasarkan metrik internal tentang "biaya" dari setiap instruksi, mengeluarkan kesimpulan yang kita butuhkan. Karena pendekatan ini, kadang-kadang terjadi bahwa callgrind tidak dapat mengenali instruksi selanjutnya dan jatuh dengan kesalahan
Instruksi tidak dikenal di alamat
Satu-satunya jalan keluar dari situasi ini adalah dengan mempertimbangkan kembali semua opsi kompilasi dan mencoba menemukan interferensi

Mari kita buat program untuk menguji alat ini, yang terdiri dari satu pustaka bersama dan satu pustaka statis (nanti kita akan menyerahkan pustaka dalam pengujian lain). Setiap perpustakaan, serta program itu sendiri, akan menyediakan fungsi komputasi yang sederhana, misalnya, perhitungan urutan Fibonacci.

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; } 


utama
 ////////////// // 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; } 


Kami mengkompilasi program, dan menjalankan valgrind sebagai berikut:

 valgrind --tool=callgrind ./test_profiler 100000000 


Kami melihat bahwa untuk pustaka statis dan fungsi reguler, hasilnya mirip dengan yang diharapkan. Tetapi di perpustakaan dinamis, callgrind tidak bisa sepenuhnya menyelesaikan fungsi.

Untuk memperbaiki ini, ketika memulai program, Anda perlu mengatur variabel LD_BIND_NOW menjadi 1, seperti ini:

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


Dan sekarang, seperti yang Anda lihat, semuanya baik-baik saja

Masalah callgrind berikutnya yang muncul dari profil dengan meniru instruksi adalah bahwa eksekusi program banyak melambat. Ini dapat membawa estimasi relatif yang salah dari waktu eksekusi berbagai bagian kode.

Mari kita lihat kode ini:

 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; } 

Di sini, saya menambahkan sejumlah kecil data ke file untuk setiap iterasi dari loop. Karena menulis ke file adalah operasi yang agak panjang, sebagai penyeimbang, saya menambahkan generasi garis dari angka ke setiap iterasi dari loop. Jelas, dalam hal ini, operasi penulisan ke file membutuhkan waktu lebih lama dari sisa logika fungsi. Tetapi callgrind berpikir secara berbeda:


Perlu juga dipertimbangkan bahwa panggilan balik hanya dapat mengukur biaya suatu fungsi saat bekerja. Fungsi tidak berfungsi - karenanya, biaya tidak meningkat. Ini menyulitkan debugging program yang dari waktu ke waktu memasukkan kunci atau bekerja dengan sistem / jaringan file pemblokiran. Mari kita periksa:

 #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; } 

Di sini kita telah melampirkan seluruh eksekusi fungsi di kunci mutex, dan memanggil fungsi ini dari dua utas yang berbeda. Hasil dari callgrind cukup dapat diprediksi - ia tidak melihat masalah dalam menangkap mutex:


Jadi, kami memeriksa beberapa masalah dalam menggunakan profiler panggilan. Mari kita beralih ke subjek tes berikutnya - profiler google perftools

google perftools


Tidak seperti callgrind, google profiler bekerja berdasarkan prinsip yang berbeda.
Alih-alih menganalisis setiap instruksi dari program yang dapat dieksekusi, itu menjeda program secara berkala dan mencoba untuk menentukan fungsi yang saat ini berada di dalamnya. Akibatnya, ini hampir tidak mempengaruhi kinerja aplikasi yang sedang berjalan. Namun pendekatan ini juga memiliki kelemahannya.

Mari kita mulai dengan membuat profil program pertama dengan dua perpustakaan.

Sebagai aturan, untuk mulai membuat profil dengan utilitas ini, Anda harus melakukan preload pustaka libprofiler.so, mengatur frekuensi sampling dan menentukan file untuk menyimpan dump. Sayangnya, profiler membutuhkan program untuk mengakhiri "sendiri." Pengakhiran paksa program akan menyebabkan laporan tidak dibuang begitu saja. Ini tidak nyaman ketika membuat profil program yang berumur panjang yang tidak berhenti, seperti daemon. Untuk mengatasi kendala ini, saya membuat skrip ini:

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* 


Utilitas ini perlu dijalankan, lewat sebagai parameter nama file yang dapat dieksekusi dan daftar parameternya. Juga, diasumsikan bahwa di sebelah skrip adalah file yang ia butuhkan libprofiler.so dan pprof. Jika program ini berumur panjang dan berhenti dengan mengganggu eksekusi, Anda harus mengatur variabel N_STOP menjadi true, misalnya, seperti ini:
 N_STOP=true ./gprof.sh ./test_profiler 10000000000 

Di akhir pekerjaan, skrip akan menghasilkan laporan dalam format callgrind favorit saya.

Jadi, mari kita jalankan program kami di bawah profiler ini.
 ./gprof.sh ./test_profiler 1000000000 


Pada prinsipnya, semuanya cukup jelas.

Seperti yang saya katakan, profiler Google bekerja dengan menghentikan eksekusi program dan menghitung fungsi saat ini. Bagaimana dia melakukannya? Dia melakukan ini dengan memutar tumpukan. Tetapi bagaimana jika, pada saat promosi tumpukan, program itu sendiri melepaskan tumpukan? Jelas, tidak ada hal baik yang akan terjadi. Mari kita periksa. Mari kita tulis fungsi seperti itu:

 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; } 

Dan jalankan profil. Program membeku dengan sangat cepat.

Ada masalah lain yang terkait dengan kekhasan operasi profiler. Misalkan kita berhasil melepaskan tumpukan, dan sekarang kita perlu mencocokkan alamat dengan fungsi spesifik dari program. Ini bisa sangat non-trivial, karena di C ++ sejumlah besar fungsi sebaris. Mari kita lihat contoh seperti ini:

 #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; } 

Tentunya, jika Anda menjalankan program misalnya seperti ini:
 ./gprof.sh ./test_profiler 1000000000 

maka fungsi func1 tidak akan pernah dipanggil. Tetapi profiler itu berpikir berbeda:

(Omong-omong, valgrind di sini memutuskan untuk tetap diam dan tidak menentukan fungsi spesifik dari mana panggilan itu berasal).

Pembuatan profil memori


Seringkali ada situasi ketika memori dari aplikasi di suatu tempat "mengalir". Jika ini disebabkan oleh kurangnya pembersihan sumber daya, maka Memcheck akan membantu mengidentifikasi masalah. Tetapi dalam C ++ modern tidak begitu sulit dilakukan tanpa manajemen sumber daya manual. unique_ptr, shared_ptr, vektor, peta membuat manipulasi bare-point menjadi sia-sia.

Namun demikian, dalam aplikasi seperti itu, memori bocor. Bagaimana kabarnya? Sederhananya, sebagai aturan, itu adalah sesuatu seperti "memasukkan nilai dalam peta berumur panjang, tetapi lupa untuk menghapusnya". Mari kita coba lacak situasi ini.

Untuk melakukan ini, kami menulis ulang fungsi pengujian kami dengan cara ini

 #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; } 

Di sini, pada setiap iterasi, kami menambahkan beberapa elemen ke peta, dan secara tidak sengaja (benar, benar) kami terkadang lupa menghapusnya dari sana. Juga, untuk menghindari mata kita, kita menyiksa std :: deque sedikit.

Kami akan menangkap kebocoran memori dengan dua alat - valgrind massif dan google heapdump .

Massif


Jalankan program dengan perintah ini
 valgrind --tool=massif ./test_profiler 1000000 

Dan kita melihat sesuatu seperti

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%) 


Dapat dilihat bahwa massif mampu mendeteksi kebocoran pada fungsinya, tetapi sejauh ini tidak jelas di mana. Mari kita membangun kembali program dengan flag -fno-inline dan menjalankan analisis lagi

massif
 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%) 


Sekarang sudah jelas di mana letak kebocoran dalam menambahkan elemen peta. Massif dapat mendeteksi objek berumur pendek, jadi manipulasi dengan std :: deque tidak terlihat dalam dump ini.

Heapdump


Agar google heapdump berfungsi, Anda perlu menautkan atau memuat pramuka perpustakaan tcmalloc . Perpustakaan ini menggantikan fungsi standar alokasi memori malloc, gratis, ... Juga, dapat mengumpulkan informasi tentang penggunaan fungsi-fungsi ini, yang akan kita gunakan untuk menganalisis program.

Karena metode ini bekerja sangat lambat (bahkan dibandingkan dengan massif), saya sarankan Anda segera menonaktifkan kompilasi fungsi dengan opsi -fno-inline saat mengkompilasi. Jadi, kami membangun kembali aplikasi kami dan berjalan bersama tim
 HEAPPROFILESIGNAL=23 HEAPPROFILE=./heap ./test_profiler 100000000 

Di sini diasumsikan bahwa pustaka tcmalloc ditautkan ke aplikasi kita.

Sekarang, kita menunggu beberapa waktu yang diperlukan untuk pembentukan kebocoran yang nyata, dan mengirimkan sinyal kepada proses kita 23
 kill -23 <pid> 

Akibatnya, sebuah file bernama heap.0001.heap muncul, yang kami konversi ke format callgrind dengan perintah

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

Juga perhatikan opsi pprof. Anda dapat memilih dari opsi inuse_space , inuse_objects , alokasi_space , alokasi_objects , yang menunjukkan ruang atau objek yang sedang digunakan, atau ruang dan objek yang dialokasikan untuk seluruh durasi program, masing-masing. Kami tertarik pada opsi inuse_space, yang menunjukkan ruang memori yang saat ini digunakan.

Buka kCacheGrind favorit kami dan lihat

std :: map telah memakan terlalu banyak memori. Mungkin ada kebocoran di dalamnya.

Kesimpulan


Membuat profil di C ++ adalah tugas yang sangat sulit. Di sini kita harus berurusan dengan fungsi inlining, instruksi yang tidak didukung, hasil yang salah, dll. Tidak selalu mungkin untuk mempercayai hasil profiler.

Selain fungsi yang diusulkan di atas, ada alat-alat lain yang dirancang untuk profil - perf, intel VTune dan lainnya. Tetapi mereka juga menunjukkan beberapa kekurangan ini. Oleh karena itu, jangan lupa tentang metode "kakek" dalam membuat profil dengan mengukur waktu eksekusi fungsi dan menampilkannya dalam log.

Juga, jika Anda memiliki teknik yang menarik untuk membuat profil kode Anda, silakan mempostingnya di komentar

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


All Articles