Огляд методик профілювання C ++
У цій відповіді я використаю кілька різних інструментів для аналізу кількох дуже простих тестових програм, щоб конкретно порівняти, як ці інструменти працюють.
Наступна програма тестування дуже проста і робить наступне:
main
дзвінки fast
і maybe_slow
3 рази, один з maybe_slow
дзвінків повільний
Повільний виклик на maybe_slow
10 разів довший і домінує під час виконання, якщо врахувати дзвінки до дочірньої функції common
. В ідеалі інструмент профілювання зможе вказати на конкретний повільний дзвінок.
як fast
і maybe_slow
виклик common
, на частку якого припадає основна частина виконання програми
Інтерфейс програми:
./main.out [n [seed]]
і програма робить O(n^2)
циклі в цілому. seed
це просто отримати різний вихід, не впливаючи на час виконання.
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 вбудований у GCC / binutils, тому все, що нам потрібно зробити, - це компілювати з -pg
опцією, щоб включити gprof. Потім ми запускаємо програму звичайно з параметром CLI розміру, який виробляє розумну тривалість виконання декількох секунд ( 10000
):
gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time ./main.out 10000
З освітніх причин ми також зробимо пробіг без включених оптимізацій. Зауважте, що на практиці це марно, оскільки зазвичай дбаєте лише про оптимізацію роботи оптимізованої програми:
gcc -pg -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out 10000
По-перше, time
розповідає, що час виконання з і без -pg
був однаковим, що чудово: жодного уповільнення! Однак я бачив облікові записи про 2–3-кратне уповільнення роботи складного програмного забезпечення, наприклад, як показано в цьому квитку .
Оскільки ми компілювали -pg
, запуск програми створює файл- gmon.out
файл, що містить дані профілювання.
Ми можемо спостерігати цей файл графічно з gprof2dot
запитом на: Чи можливо отримати графічне зображення результатів gprof?
sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof main.out > main.gprof
gprof2dot < main.gprof | dot -Tsvg -o output.svg
Тут gprof
інструмент зчитує gmon.out
інформацію про сліди та формує звіт, прочитаний людиною main.gprof
, який gprof2dot
потім читає, щоб створити графік.
Джерело для gprof2dot знаходиться за адресою: https://github.com/jrfonseca/gprof2dot
Ми спостерігаємо за -O0
ходом наступного :
і для -O3
пробігу:
Вихідний -O0
результат значною мірою пояснює себе. Наприклад, це показує, що 3 maybe_slow
виклики та їхні дочірні дзвінки займають 97,56% від загальної тривалості виконання, хоча саме на виконання maybe_slow
без дітей становить 0,00% від загального часу виконання, тобто майже весь час, проведений на цій функції, витрачався на дитячі дзвінки.
TODO: чому main
відсутній у -O3
висновку, навіть якщо я можу бачити його bt
в GDB? Відсутня функція з виходу GProf. Я думаю, це тому, що gprof також є вибірковою базою на додаток до складеного інструментарію, і -O3
main
це просто занадто швидко і не має зразків.
Я вибираю вихід SVG замість PNG, оскільки SVG можна шукати за допомогою Ctrl + F, а розмір файлу може бути приблизно в 10 разів меншим. Також ширина та висота згенерованого зображення можуть бути гумористичними з десятками тисяч пікселів для складного програмного забезпечення, і GNOME eog
3.28.1 виправляється в цьому випадку для PNG, в той час як SVG-файли відкриваються моїм браузером автоматично. gimp 2.8 добре працював, дивіться також:
але навіть тоді ви будете багато перетягувати зображення, щоб знайти те, що хочете, див. наприклад, це зображення із "справжнього" прикладу програмного забезпечення, взятого з цього квитка :
Чи можете ви легко знайти найкритичніший стек викликів за допомогою всіх тих крихітних несортових ліній спагетті, які перетинаються один з одним? Можливо, є кращі dot
варіанти, я впевнений, але зараз я не хочу йти туди. Нам дійсно потрібен відповідний переглядач для цього, але я його ще не знайшов:
Однак ви можете використовувати кольорову карту, щоб трохи пом'якшити ці проблеми. Наприклад, на попередньому величезному зображенні мені нарешті вдалося знайти критичний шлях ліворуч, коли я зробив геніальну відрахування, що зелений приходить після червоного, за ним нарешті темніший і темніший синій.
Крім того, ми можемо також спостерігати виведення тексту gprof
вбудованого інструменту binutils, який ми раніше зберегли на:
cat main.gprof
За замовчуванням це дає надзвичайно багатослівний вихід, який пояснює, що означає вихідні дані. Оскільки я не можу пояснити краще, ніж це, я дозволяю вам це прочитати самостійно.
Після того, як ви зрозуміли формат виведення даних, ви можете зменшити багатослівність, щоб показувати лише дані без уроку з -b
можливістю:
gprof -b main.out
У нашому прикладі результати були для -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
і для -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
Як дуже швидкий підсумок для кожного розділу, наприклад:
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]
центрів навколо функції, яка залишена відступною ( maybe_flow
). [3]
ідентифікатор цієї функції. Над функцією знаходяться його викликаючі, а нижче - виклики.
Бо -O3
дивіться тут, як у графічному виході, що maybe_slow
і fast
не має відомого батьківського, що означає документація <spontaneous>
.
Я не впевнений, чи є хороший спосіб зробити покрокове профілювання за допомогою gprof: `gprof` час, витрачений на конкретні рядки коду
valgrind callgrind
valgrind запускає програму через віртуальну машину valgrind. Це робить профілювання дуже точним, але також призводить до дуже великого уповільнення програми. Раніше я також згадав про kcachegrind в: Інструменти для отримання графічної графіки виклику функції коду
callgrind - це інструмент valgrind для профілювання коду, а kcachegrind - програма KDE, яка може візуалізувати вихід кешгринда.
Спочатку нам потрібно зняти -pg
прапор, щоб повернутися до звичайної компіляції, інакше запуск насправді не вдається Profiling timer expired
, і так, це так часто, що я це зробив, і для нього виникло запитання щодо переповнення стека.
Отже, ми компілюємо і запускаємо як:
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
Я вмикаю, --dump-instr=yes --collect-jumps=yes
тому що це також скидає інформацію, яка дозволяє нам переглянути порядок продуктивності за збірною лінією при порівняно невеликих додаткових накладних витратах.
Повертаючись, time
говорить нам, що на виконання програми було потрібно 29,5 секунд, тому у нас на цьому прикладі було сповільнення приблизно в 15 разів. Зрозуміло, що це уповільнення стане серйозним обмеженням для великих навантажень. На згадуваному тут "прикладі реального програмного забезпечення" я спостерігав уповільнення 80x.
Програма генерує файл даних профілю, названий, callgrind.out.<pid>
наприклад, callgrind.out.8554
у моєму випадку. Ми переглядаємо цей файл за допомогою:
kcachegrind callgrind.out.8554
який показує графічний інтерфейс, що містить дані, схожі на текстовий вихід gprof:
Крім того, якщо ми переходимо на праву нижню вкладку "Графік викликів", ми бачимо графік викликів, який ми можемо експортувати, натиснувши правою кнопкою миші, щоб отримати наступне зображення з необгрунтованою кількістю білої рамки :-)
Я думаю, що fast
це не відображається на цьому графіку, оскільки kcachegrind повинен спростити візуалізацію, оскільки цей виклик займає занадто мало часу, швидше за все, це буде поведінка, яку ви хочете в реальній програмі. У меню правої кнопки миші встановлено деякі налаштування, щоб визначити, коли потрібно скинути такі вузли, але я не міг змусити його відображати такий короткий дзвінок після швидкої спроби. Якщо я натискаю на fast
ліве вікно, воно показує графік виклику з fast
, так що цей стек був фактично захоплений. Ніхто ще не знайшов способу показати повний графік виклику графіка: Зробити callgrind показати всі виклики функцій у callgraph kcachegrind
TODO на складному програмному забезпеченні C ++, я бачу деякі записи типу <cycle N>
, наприклад, <cycle 11>
де я очікую назви функцій, що це означає? Я помітив, що є кнопка "Визначення циклу", щоб увімкнути та вимкнути цю функцію, але що це означає?
perf
з linux-tools
perf
Схоже, використовують виключно механізми відбору ядра Linux. Це робить його дуже простим у налаштуванні, але також не повністю точним.
sudo apt install linux-tools
time perf record -g ./main.out 10000
Це додало 0,2 секунди до виконання, тому ми чудово розуміємо час, але я все ще не бачу особливого інтересу після розширення common
вузла стрілкою правої клавіатури:
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
Тоді я намагаюся орієнтувати -O0
програму, щоб побачити, чи це щось показує, і лише зараз, нарешті, я бачу графік викликів:
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: що сталося на -O3
страті? Це просто так maybe_slow
і fast
були занадто швидкими і не отримали жодних зразків? Чи добре це працює з -O3
більш великими програмами, на виконання яких потрібно більше часу? Я пропустив якийсь варіант CLI? Я дізнався про те, -F
щоб контролювати частоту вибірки в Герц, але я встановив її до максимальної, дозволеної за замовчуванням -F 39500
(може бути збільшена за sudo
), і я все ще не бачу чітких дзвінків.
perf
Хороша річ - це інструмент FlameGraph від Брендана Грегга, який дуже акуратно відображає таймінги стеків викликів, що дозволяє швидко бачити великі дзвінки. Інструмент доступний по адресою: https://github.com/brendangregg/FlameGraph і також згадується в його перфорації підручника по адресою: http://www.brendangregg.com/perf.html#FlameGraphs Коли я біг , perf
НЕ sudo
я ERROR: No stack counts found
так для тепер я буду це робити з 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
але в такій простій програмі вихід не дуже простий для розуміння, оскільки ми не можемо легко побачити maybe_slow
ні fast
на цьому графіку:
На більш складному прикладі стає зрозуміло, що означає графік:
TODO [unknown]
в цьому прикладі є журнал функцій, чому це так?
Інший інтерфейс інтерфейсу Perf, який, можливо, варто його включати:
Плагін Eclipse Trace Compass: https://www.eclipse.org/tracecompass/
Але це є і недоліком того, що вам потрібно спершу перетворити дані у загальний формат сліду, що можна зробити за допомогою perf data --to-ctf
, але це потрібно ввімкнути під час створення / мати perf
достатньо новий, будь-який з цих випадків не стосується перф. Ubuntu 18.04
https://github.com/KDAB/hotspot
Мінусом цього є те, що, здається, немає пакету Ubuntu, і для його створення потрібен Qt 5.10, тоді як Ubuntu 18.04 знаходиться на рівні Qt 5,9.
gperftools
Раніше називався "Інструменти ефективності Google", джерело: https://github.com/gperftools/gperftools На основі зразків.
Спочатку встановіть gperftools за допомогою:
sudo apt install google-perftools
Тоді ми можемо включити процесор gperftools CPU двома способами: під час виконання або під час збирання.
Під час виконання ми повинні передати встановлену LD_PRELOAD
точку libprofiler.so
, яку ви можете знайти locate libprofiler.so
, наприклад, у моїй системі:
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
Крім того, ми можемо створити бібліотеку в час зв’язку, розподіляючи проходження LD_PRELOAD
під час виконання:
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
Дивіться також: gperftools - файл профілю не скидається
Найприємніший спосіб перегляду цих даних, які я знайшов до цього часу, - це зробити вихід pprof у тому самому форматі, який kcachegrind приймає як вхідний (так, Valgrind-project-viewer-tool) і використовувати kcachegrind для перегляду того:
google-pprof --callgrind main.out prof.out > callgrind.out
kcachegrind callgrind.out
Після запуску будь-якого з цих методів, ми отримуємо prof.out
файл даних профілю як вихід. Ми можемо розглядати цей файл графічно як SVG за допомогою:
google-pprof --web main.out prof.out
який дає як звичний графік виклику, як і інші інструменти, але з незграбною одиницею кількості вибірок, а не секунд.
Крім того, ми також можемо отримати деякі текстові дані за допомогою:
google-pprof --text main.out prof.out
що дає:
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
Дивіться також: Як користуватися інструментами для парфуму Google
Тестовано в Ubuntu 18.04, gprof2dot 2019.11.30, valgrind 3.13.0, perf 4.15.18, ядро Linux 4.15.0, FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b, gperftools 2.5-2.