Survei teknik profiling C ++
Dalam jawaban ini, saya akan menggunakan beberapa alat berbeda untuk menganalisis beberapa program pengujian yang sangat sederhana, untuk membandingkan secara konkret cara kerja alat tersebut.
Program pengujian berikut ini sangat sederhana dan melakukan hal berikut:
main
panggilan fast
dan maybe_slow
3 kali, salah satu maybe_slow
panggilan menjadi lambat
Panggilan lambat maybe_slow
adalah 10x lebih lama, dan mendominasi runtime jika kami mempertimbangkan panggilan ke fungsi anak common
. Idealnya, alat profiling akan dapat mengarahkan kita ke panggilan lambat tertentu.
keduanya fast
dan maybe_slow
panggilan common
, yang menyumbang sebagian besar pelaksanaan program
Antarmuka program adalah:
./main.out [n [seed]]
dan program ini melakukan O(n^2)
loop secara total. seed
hanya untuk mendapatkan output yang berbeda tanpa mempengaruhi runtime.
main.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 membutuhkan kompilasi ulang perangkat lunak dengan instrumentasi, dan itu juga menggunakan pendekatan pengambilan sampel bersama dengan instrumentasi itu. Karena itu, ia menemukan keseimbangan antara akurasi (pengambilan sampel tidak selalu sepenuhnya akurat dan dapat melewati fungsi) dan pelambatan eksekusi (instrumentasi dan pengambilan sampel adalah teknik yang relatif cepat yang tidak terlalu memperlambat eksekusi).
gprof terintegrasi dengan GCC / binutils, jadi yang harus kita lakukan adalah mengkompilasi dengan -pg
opsi untuk mengaktifkan gprof. Kami kemudian menjalankan program secara normal dengan parameter CLI ukuran yang menghasilkan jangka waktu yang wajar beberapa detik ( 10000
):
gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time ./main.out 10000
Untuk alasan pendidikan, kami juga akan menjalankan tanpa mengaktifkan optimisasi. Perhatikan bahwa ini tidak berguna dalam praktik, karena Anda biasanya hanya peduli untuk mengoptimalkan kinerja program yang dioptimalkan:
gcc -pg -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out 10000
Pertama, time
beri tahu kami bahwa waktu eksekusi dengan dan tanpa -pg
sama, yang hebat: tidak ada perlambatan! Namun saya telah melihat akun perlambatan 2x - 3x pada perangkat lunak yang kompleks, misalnya seperti yang ditunjukkan dalam tiket ini .
Karena kami kompilasi dengan -pg
, menjalankan program menghasilkan file gmon.out
file yang berisi data profil.
Kita dapat mengamati file itu secara grafis dengan gprof2dot
seperti yang ditanyakan di: Apakah mungkin untuk mendapatkan representasi grafis dari hasil gprof?
sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof main.out > main.gprof
gprof2dot < main.gprof | dot -Tsvg -o output.svg
Di sini, gprof
alat membaca gmon.out
informasi jejak, dan menghasilkan laporan yang dapat dibaca manusia main.gprof
, yang gprof2dot
kemudian dibaca untuk menghasilkan grafik.
Sumber untuk gprof2dot ada di: https://github.com/jrfonseca/gprof2dot
Kami mengamati hal-hal berikut untuk -O0
pelarian:
dan untuk -O3
pelarian:
The -O0
output cukup banyak jelas. Sebagai contoh, ini menunjukkan bahwa 3 maybe_slow
panggilan dan panggilan anak mereka mengambil 97,56% dari total runtime, meskipun eksekusi maybe_slow
itu sendiri tanpa anak-anak menyumbang 0,00% dari total waktu eksekusi, yaitu hampir semua waktu yang dihabiskan dalam fungsi tersebut dihabiskan untuk panggilan anak.
TODO: mengapa main
hilang dari -O3
output, meskipun saya bisa melihatnya di bt
dalam GDB? Fungsi yang hilang dari keluaran GProf saya pikir itu karena gprof juga berdasarkan sampel selain instrumentasi yang dikompilasi, dan -O3
main
terlalu cepat dan tidak punya sampel.
Saya memilih output SVG daripada PNG karena SVG dapat dicari dengan Ctrl + F dan ukuran file bisa sekitar 10x lebih kecil. Selain itu, lebar dan tinggi gambar yang dihasilkan dapat menjadi sangat muda dengan puluhan ribu piksel untuk perangkat lunak yang kompleks, dan eog
bug GNOME 3.28.1 keluar dalam hal itu untuk PNG, sementara SVG dibuka oleh browser saya secara otomatis. gimp 2.8 bekerja dengan baik, lihat juga:
tetapi meskipun demikian, Anda akan banyak menyeret gambar di sekitar untuk menemukan apa yang Anda inginkan, lihat misalnya gambar ini dari contoh perangkat lunak "nyata" yang diambil dari tiket ini :
Dapatkah Anda menemukan tumpukan panggilan yang paling penting dengan mudah dengan semua garis spaghetti kecil yang tidak disortir saling bertabrakan? Mungkin ada dot
opsi yang lebih baik saya yakin, tetapi saya tidak ingin pergi ke sana sekarang. Yang benar-benar kita butuhkan adalah pemirsa khusus yang berdedikasi untuk itu, tetapi saya belum menemukannya:
Namun Anda dapat menggunakan peta warna untuk sedikit mengurangi masalah tersebut. Misalnya, pada gambar besar sebelumnya, saya akhirnya berhasil menemukan jalur kritis di sebelah kiri ketika saya membuat deduksi cemerlang bahwa hijau muncul setelah merah, diikuti akhirnya dengan biru yang lebih gelap dan lebih gelap.
Atau, kita juga dapat mengamati keluaran teks dari gprof
alat binutils bawaan yang sebelumnya kita simpan di:
cat main.gprof
Secara default, ini menghasilkan output yang sangat verbose yang menjelaskan apa artinya data output. Karena saya tidak bisa menjelaskan lebih baik dari itu, saya akan membiarkan Anda membacanya sendiri.
Setelah Anda memahami format output data, Anda dapat mengurangi verbositas untuk hanya menampilkan data tanpa tutorial dengan -b
opsi:
gprof -b main.out
Dalam contoh kami, output untuk -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
dan untuk -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
Sebagai ringkasan yang sangat cepat untuk setiap bagian, misalnya:
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]
berpusat di sekitar fungsi yang dibiarkan berlekuk ( maybe_flow
). [3]
adalah ID dari fungsi itu. Di atas fungsi, ada peneleponnya, dan di bawahnya ada callees.
Untuk -O3
, lihat di sini seperti pada keluaran grafis itu maybe_slow
dan fast
tidak memiliki orangtua yang dikenal, yang merupakan arti dari dokumentasi itu <spontaneous>
.
Saya tidak yakin apakah ada cara yang baik untuk melakukan profil garis-demi-baris dengan gprof: `gprof` waktu yang dihabiskan dalam baris kode tertentu
valgrind callgrind
valgrind menjalankan program melalui mesin virtual valgrind. Ini membuat profil sangat akurat, tetapi juga menghasilkan perlambatan program yang sangat besar. Saya juga telah menyebutkan kcachegrind sebelumnya di: Alat untuk mendapatkan grafik panggilan fungsi bergambar kode
callgrind adalah alat valgrind untuk kode profil dan kcachegrind adalah program KDE yang dapat memvisualisasikan output cachegrind.
Pertama kita harus menghapus -pg
flag untuk kembali ke kompilasi normal, jika tidak run sebenarnya gagal Profiling timer expired
, dan ya, ini sangat umum yang saya lakukan dan ada pertanyaan Stack Overflow untuk itu.
Jadi kami mengkompilasi dan menjalankan sebagai:
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
Saya mengaktifkan --dump-instr=yes --collect-jumps=yes
karena ini juga membuang informasi yang memungkinkan kami melihat gangguan kinerja per jalur perakitan, dengan biaya overhead tambahan yang relatif kecil.
Off the bat, time
memberitahu kami bahwa program ini membutuhkan waktu 29,5 detik untuk dieksekusi, jadi kami mengalami penurunan sekitar 15x pada contoh ini. Jelas, perlambatan ini akan menjadi batasan serius untuk beban kerja yang lebih besar. Pada "contoh perangkat lunak dunia nyata" yang disebutkan di sini , saya mengamati penurunan 80x.
Proses menghasilkan file data profil bernama callgrind.out.<pid>
eg callgrind.out.8554
dalam kasus saya. Kami melihat file itu dengan:
kcachegrind callgrind.out.8554
yang menunjukkan GUI yang berisi data yang mirip dengan output gprof tekstual:
Juga, jika kita pergi ke kanan bawah "Grafik Panggilan" tab, kita melihat grafik panggilan yang dapat kita ekspor dengan mengklik kanan untuk mendapatkan gambar berikut dengan jumlah batas putih yang tidak masuk akal :-)
Saya pikir fast
tidak muncul pada grafik itu karena kcachegrind pasti telah menyederhanakan visualisasi karena panggilan itu memakan waktu terlalu sedikit, ini kemungkinan akan menjadi perilaku yang Anda inginkan pada program nyata. Menu klik kanan memiliki beberapa pengaturan untuk mengontrol kapan harus menyisihkan node seperti itu, tetapi saya tidak bisa membuatnya untuk menampilkan panggilan singkat setelah upaya cepat. Jika saya mengklik pada fast
jendela kiri, itu memang menampilkan grafik panggilan fast
, sehingga tumpukan itu benar-benar ditangkap. Belum ada yang menemukan cara untuk menampilkan grafik panggilan grafik lengkap: Buat panggilan panggil tunjukkan semua panggilan fungsi di kcachegrind callgraph
TODO pada perangkat lunak C ++ yang kompleks, saya melihat beberapa entri tipe <cycle N>
, misalnya di <cycle 11>
mana saya mengharapkan nama fungsi, apa artinya itu? Saya perhatikan ada tombol "Deteksi Siklus" untuk menghidupkan dan mematikannya, tetapi apa artinya?
perf
dari linux-tools
perf
tampaknya menggunakan mekanisme pengambilan sampel kernel Linux secara eksklusif. Ini membuatnya sangat mudah untuk diatur, tetapi juga tidak sepenuhnya akurat.
sudo apt install linux-tools
time perf record -g ./main.out 10000
Ini menambahkan 0,2s ke eksekusi, jadi kami baik-baik saja waktu, tapi saya masih tidak melihat banyak minat, setelah memperluas common
node dengan panah kanan keyboard:
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
Jadi saya mencoba membuat tolok ukur -O0
program untuk melihat apakah itu menunjukkan sesuatu, dan hanya sekarang, akhirnya, saya melihat grafik panggilan:
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: apa yang terjadi pada -O3
eksekusi? Apakah hanya itu maybe_slow
dan fast
terlalu cepat dan tidak mendapatkan sampel? Apakah ini bekerja dengan baik -O3
pada program yang lebih besar yang membutuhkan waktu lebih lama untuk dijalankan? Apakah saya melewatkan beberapa opsi CLI? Saya menemukan tentang -F
untuk mengontrol frekuensi sampel dalam Hertz, tetapi saya mengubahnya hingga maks diizinkan secara default -F 39500
(dapat ditingkatkan dengan sudo
) dan saya masih tidak melihat panggilan yang jelas.
Satu hal keren tentang perf
alat FlameGraph dari Brendan Gregg yang menampilkan pengaturan waktu panggilan dengan cara yang sangat rapi yang memungkinkan Anda untuk dengan cepat melihat panggilan besar. Alat ini tersedia di: https://github.com/brendangregg/FlameGraph dan juga disebutkan pada nya Perf tutorial di: http://www.brendangregg.com/perf.html#FlameGraphs Ketika aku berlari perf
tanpa sudo
aku ERROR: No stack counts found
jadi untuk sekarang saya akan melakukannya dengan 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
tetapi dalam program sederhana seperti itu, keluarannya tidak mudah dipahami, karena kita tidak dapat dengan mudah melihat maybe_slow
maupun fast
pada grafik itu:
Pada contoh yang lebih kompleks, menjadi jelas apa arti grafik:
TODO ada log [unknown]
fungsi dalam contoh itu, mengapa begitu?
Antarmuka GUI perf lain yang mungkin layak termasuk:
Plugin Eclipse Trace Compass: https://www.eclipse.org/tracecompass/
Tetapi ini memiliki kelemahan yaitu Anda harus terlebih dahulu mengonversi data ke Format Jejak Umum, yang dapat dilakukan dengan perf data --to-ctf
, tetapi harus diaktifkan pada waktu membangun / memiliki perf
cukup baru, yang keduanya tidak berlaku untuk perf di Ubuntu 18.04
https://github.com/KDAB/hotspot
Kelemahan dari ini adalah bahwa sepertinya tidak ada paket Ubuntu, dan membangunnya membutuhkan Qt 5.10 sedangkan Ubuntu 18.04 adalah pada Qt 5.9.
gperftools
Sebelumnya disebut "Google Performance Tools", sumber: https://github.com/gperftools/gperftools Berbasis sampel.
Pertama instal gperftools dengan:
sudo apt install google-perftools
Kemudian, kita dapat mengaktifkan profiler CPU gperftools dalam dua cara: saat runtime, atau saat membangun.
Pada saat runtime, kita harus melewati set LD_PRELOAD
to point ke libprofiler.so
, yang dapat Anda temukan locate libprofiler.so
, misalnya di sistem saya:
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
Atau, kita dapat membangun pustaka pada saat tautan, membagikan lewat LD_PRELOAD
saat runtime:
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
Lihat juga: gperftools - file profil tidak dibuang
Cara terbaik untuk melihat data ini yang saya temukan sejauh ini adalah membuat pprof output dalam format yang sama dengan yang diambil oleh kcachegrind sebagai input (ya, alat Valgrind-project-viewer-tool) dan gunakan kcachegrind untuk melihat bahwa:
google-pprof --callgrind main.out prof.out > callgrind.out
kcachegrind callgrind.out
Setelah berjalan dengan salah satu metode tersebut, kami mendapatkan prof.out
file data profil sebagai output. Kita dapat melihat file itu secara grafis sebagai SVG dengan:
google-pprof --web main.out prof.out
yang memberi sebagai grafik panggilan akrab seperti alat lain, tetapi dengan unit kikuk jumlah sampel daripada detik.
Atau, kita juga bisa mendapatkan beberapa data tekstual dengan:
google-pprof --text main.out prof.out
pemberian yang mana:
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
Lihat juga: Cara menggunakan alat google perf
Diuji di Ubuntu 18.04, gprof2dot 2019.11.30, valgrind 3.13.0, perf 4.15.18, kernel Linux 4.15.0, FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b, gperftools 2.5-2.