Обзор методов профилирования C ++
В этом ответе я буду использовать несколько различных инструментов для анализа нескольких очень простых тестовых программ, чтобы конкретно сравнить, как эти инструменты работают.
Следующая тестовая программа очень проста и выполняет следующие действия:
mainзвонки fastи maybe_slow3 раза, один из maybe_slowзвонков медленный
Медленный вызов в maybe_slow10 раз длиннее и доминирует во время выполнения, если мы рассмотрим вызовы дочерней функции 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было одинаковым, и это здорово: никакого замедления! Однако я видел сообщения о 2x - 3x замедлениях на сложном программном обеспечении, например, как показано в этом билете .
Поскольку мы скомпилировали, при -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 eog3.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, которая может визуализировать вывод cachegrind.
Сначала мы должны убрать -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 раз в этом примере. Очевидно, что это замедление станет серьезным ограничением для больших рабочих нагрузок. На упомянутом здесь «примере программного обеспечения реального мира» я наблюдал замедление в 80 раз.
Прогон генерирует файл данных профиля с именем, callgrind.out.<pid>например, callgrind.out.8554в моем случае. Мы просматриваем этот файл с:
kcachegrind callgrind.out.8554
который показывает GUI, который содержит данные, аналогичные текстовому выводу gprof:

Кроме того, если мы перейдем в правую нижнюю вкладку «График вызовов», мы увидим график вызовов, который можно экспортировать, щелкнув его правой кнопкой мыши, чтобы получить следующее изображение с необоснованным количеством белой границы :-)

Я думаю, что fastне отображается на этом графике, потому что kcachegrind, должно быть, упростил визуализацию, потому что этот вызов занимает слишком мало времени, это, вероятно, будет поведение, которое вы хотите в реальной программе. Меню правого клика имеет некоторые настройки, чтобы контролировать, когда отбирать такие узлы, но я не смог заставить его показать такой короткий вызов после быстрой попытки. Если я нажимаю на fastлевое окно, оно отображает граф вызовов fast, так что стек фактически был захвачен. Никто еще не нашел способ показать полный график вызовов графа: заставить callgrind показывать все вызовы функций в графе вызовов 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
ТОДО: что случилось на -O3казни? Это просто так, maybe_slowи они fastбыли слишком быстры и не получили никаких образцов? Хорошо ли работает с -O3большими программами, выполнение которых занимает больше времени? Я пропустил какую-то опцию CLI? Я узнал, -Fкак управлять частотой дискретизации в герцах, но я увеличил ее до максимально допустимого значения -F 39500(может быть увеличено с помощью sudo), и я до сих пор не вижу четких вызовов.
Еще одна интересная вещь perf- это инструмент FlameGraph от Brendan Gregg, который отображает время стека вызовов очень аккуратно, что позволяет быстро видеть большие вызовы. Инструмент доступен по адресу: 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 GUI, которые могут стоить того, включают:
Плагин 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 Performance", источник: https://github.com/gperftools/gperftools Пример на основе.
Сначала установите gperftools с помощью:
sudo apt install google-perftools
Затем мы можем включить профилировщик ЦП gperftools двумя способами: во время выполнения или во время сборки.
Во время выполнения мы должны передать set LD_PRELOADto point 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 Perf Tools
Протестировано в 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.