Обзор методов профилирования 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
было одинаковым, и это здорово: никакого замедления! Однако я видел сообщения о 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 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, которая может визуализировать вывод 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_PRELOAD
to 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.