5. Анализ производительности программ

В этом разделе рассматриваются инструменты для анализа производительности кода на платформе «Эльбрус».

Важнейшие утилиты для работы с производительностью - профилировщики. Под профилем понимается информация о времени и частоте исполнения различных участков программы. Возможна различная детализация профиля:

  • с точностью до процедуры;
  • с точностью до линейных участков;
  • с точностью до отдельных команд;
  • с точностью до состояния конвейера при исполнении отдельных команд.

Профиль можно получать с помощью:

  • симулятора / модели процессора;
  • инструментирующих профилировщиков:
  • сэмплинговых профилировщиков:

Рассмотрим две утилиты для сбора сэмплов в ОС «Эльбрус Линукс»:

  • dprof (разработан в АО «МЦСТ»);
  • perf (поддержана утилита и подсистема в ядре ОС).

Сэмпл - информация о состоянии программы в момент регулярно-случайной остановки. Минимальная информация в сэмпле - IP адрес текущей команды.

Профилировщик dprof запускает программу в качестве ptrace-наблюдаемой и перехватывает прерывания для сбора/сброса сэмпла. При этом каждое прерывание приводит к смене контекста «программа»/dprof для того, чтобы dprof сбросил сэмпл. Профилировщик perf имеет поддержку в ядре ОС, и смены контекста в этом случае не происходит - сбор и сброс сэмпла выполняются при обработке прерывания в ядре ОС.

Рассмотрим примеры использования профилировщиков. В обзоре будут представлены не все возможности, а лишь активно используемые на платформе Эльбрус для сбора информации о потреблении ресурсов.

5.1. Утилита perf

Для записи сэмплов надо использовать команду:

perf record

Следом за ней указывается тип события, по которому будет происходить сбор. Оптимальным для платформы Эльбрус является тип task-clock. Потом через параметр -F указывается необходимая частота срабатывания: сколько раз в секунду программа останавливается для сбора информации о выполняемой функции. Рекомендации по выбору числа для -F могут быть следующими:

  • при времени выполнения программы несколько минут и более: -F10 -F100
  • при выполнении программы менее одной минуты: -F1000
  • при выполнении программы менее одной секунды: -F10000.

Так можно собрать сэмплы для программы a.O3:

perf record -e task-clock -F10000 ./a.O3

Выдача:

[ perf record: Woken up 2 times to write data ]
[ perf record: Captured and wrote 0.340 MB perf.data (8818 samples)

Стоит следить за тем, чтобы количество собранных сэмплов не было слишком маленьким.

Чтобы посмотреть собранный профиль, необходимо выполнить команду:

perf report

Профиль имеет вид:

Samples: 8K of event 'task-clock', Event count (approx.): 881800000
Overhead       Samples  Command  Shared Object      Symbol
99,65%          8787 a.O3     a.O3               [.] sample
0,09%             8  a.O3     a.O3               [.] main
0,02%             2  a.O3     [kernel.kallsyms]  [k] page_add_file_rmap
0,02%             2  a.O3     [kernel.kallsyms]  [k] release_pages
0,02%             2  a.O3     [kernel.kallsyms]  [k] zap_pte_range
0,01%             1  a.O3     [kernel.kallsyms]  [k] __flush_icache_pag

Два раза нажимая Enter на имени конкретной функции, мы можем посмотреть дизассемблер. Для возврата к профилю нужно нажать “q”. В данном примере видно, что дольше всех работает функция sample, соответственно ее и стоит анализировать для повышения производительности.

5.2. Утилита dprof

Рассмотрим профилировщик dprof.

dprof -o IP_raw ${exe} params...

В файле IP_raw будет находиться список адресов Instruction Pointer-ов (IP), полученный с интервалами 10ms. Частоту можно менять опцией -d.

[14684] 0000455555786910
[14684] 0000000000036d38
[14684] 000000000007c968

По опции -b можно сбрасывать не только IP, но и весь стек адресов возврата. Это позволит в дальнейшем получить профиль с учетом графа вызовов.

[14716] 0000000000011400
[14716] DPROF-BACKTRACE EVENT timer:10 (depth=12): 0000000000011400 0000000000063898 0000000000038d00 0000000000039258 000000000003b778 000000000002faa8 0000000000030f80 0000000000071298 0000000000070100 000045555560e758
[14716] 0000000000059408
[14716] DPROF-BACKTRACE EVENT timer:10 (depth=9):  0000000000059408 000000000003b818 000000000002faa8 0000000000030f80 0000000000071298 0000000000070100 000045555560e758

Для получения текстового профиля из IP_raw можно использовать команды:

dprof2ptrace.sh IP_raw >${exe}.ptrace
ldis -P ${exe}

Пример вывода от команд выше:

.symtab:
%time      %summ      time    calls  time/call  name
25.81%     25.81%       24        1       24    mrglist
12.90%     38.71%       12        1       12    addlist
6.45%     45.16%        6        1        6    getefflibs
3.23%     48.39%        3        1        3    lupdate

Для визуализации профиля с использованием информации о графе вызовов можно использовать формат callgrind/cachegrind:

dprof2callgrind.sh ${exe} IP_raw >callgrind.out

С помощью сэмплинговых профилировщиков можно получать доступ к информации о блокировках и их причинах.

Во всех современных микропроцессорах встроены отладочно-диагностические мониторные регистры для профилирования различных событий. Можно использовать эти мониторные регистры для выработки прерываний, которые перехватывает сэмплинговый профилировщик.

Примеры:

  • TICKS – все такты
  • EXEC – исполненные команды
  • BUB_B – блокировки конвейера на стадии B
  • BUB_E2 – блокировки конвейера на стадии E2
  • IB_NO_COMMAND – блокировки конвейера на стадии подкачки кода
  • L2_QUERY – запросы в L2$
  • L2_HIT – попадания в L2$

Для сбора профиля по событиям нужно задать событие и частоту выработки прерываний SAV (Sample After Value):

dprof -o IP_raw_exec -e EXEC:10000000  ${exe} params...

Прерывания будут вырабатываться аппаратурой через каждые 10^7 выполненных команд.

Можно задать до 4 событий одновременно, каждое из них будет вырабатывать прерывания с собственным отличительным маркером.

Рассмотрим пример работы программы bc, которая вычисляет математическое выражение.

hostname /export/minis/Dprof # time -p echo 2^218839%2 | bc
0
real 1.00
user 1.00
sys 0.00

Для удобства числа подобраны так, чтобы время выполнения равнялось одной секунде. Так как машина, на которой выполняется выражение, работает с тактовой частотой 1200MHz, это значит, что за одну секунду будет выполнено миллиард двести миллионов тактов, часть из которых пойдет на полезные вычисления, часть на задержки.

Рассмотрим профиль выполнения примера:

echo 2^218839%2 | dprof -o IP_raw -b -f /usr/bin/bc
-- child 9530 attached
0
-- child 9530 exited with code
dprof2ptrace.sh IP_raw >bc.ptrace
cp /usr/bin/bc .
ldis -P ./bc
./bc:
.symtab:
%time      %summ      time    calls  time/call  name
46.00%     46.00%       46        1       46    _bc_simp_mul
37.00%     83.00%       37        1       37    _bc_shift_addsub
 7.00%     90.00%        7        1        7    (*) external unknown
 4.00%     94.00%        4        1        4    _bc_do_sub
 2.00%     96.00%        2        1        2    _bc_rec_mul
 1.00%     97.00%        1        1        1    bc_new_num
 1.00%     98.00%        1        1        1    bc_free_num
 1.00%     99.00%        1        1        1    free@plt
 1.00%    100.00%        1        1        1    memset@plt

Если мы сравним этот профиль с полученным командой perf:

45,73%          4749  bc       bc                 [.] _bc_shift_addsub
42,43%          4407  bc       bc                 [.] _bc_simp_mul
 3,76%           390  bc       bc                 [.] _bc_do_sub
 1,65%           171  bc       bc                 [.] _bc_rec_mul
 1,45%           151  bc       libc-2.29.so       [.] cfree@GLIBC_2.2
 1,15%           119  bc       bc                 [.] bc_free_num
 0,87%            90  bc       libc-2.29.so       [.] malloc
 0,51%            53  bc       libc-2.29.so       [.] memset

то видим, что профили различаются, но не существенно. Две самые тяжелые функции в обоих профилировщиках одни и те же.

Посмотрим, сколько тактов ушло на различные события при выполнении примера. Узнаем общее количество потраченных тактов:

echo 2^218839%2 | /opt/mcst/bin/dprof -m TICKS /usr/bin/bc
-- child 9566 attached
0
-- child 9566 exited with code 0
[9566] event TICKS (2): 1195620487

Рассмотрим, какую долю этих тактов выполнялся код:

echo 2^218839%2 | /opt/mcst/bin/dprof -m EXEC /usr/bin/bc
-- child 9569 attached
0
-- child 9569 exited with code 0
[9569] event EXEC (2): 82864377

Т.е. 1195620487 - 828643773 = 366976714 тактов потрачено на обслуживание вычислений – отсутствие кода и другие задержки.

Рассмотрим другие счётчики. На отсутствие команд пришлось порядка 5 миллионов тактов.

echo 2^218839%2 | /opt/mcst/bin/dprof -m IB_NO_COMMAND /usr/bin/bc
-- child 9582 attached
0
-- child 9582 exited with code 0
[9582] event IB_NO_COMMAND (2): 5252756

На задержки от BUB_E2 ушло порядка 55 миллионов тактов:

echo 2^218839%2 | /opt/mcst/bin/dprof -m BUB_E2 /usr/bin/bc
-- child 9588 attached
0
-- child 9588 exited with code 0
[9588] event BUB_E2 (2): 55073991

Таким образом можно просмотреть большое количество событий и проанализировать, что больше всего может повлиять на производительность. Полный список доступных событий для анализа можно посмотреть, выполнив команду:

dprof -mlist

5.3. Функции gcc

Компилятор gcc предоставляет опцию -gline, которая позволяет получить информацию о ходе выполнения программы.

Пример:

! function 'sample', entry = 68, value = 0x0105a0, size = 0x228, sect = ELF_TEXT num = 13
0000<0000000105a0> sample:
                    addd,1 0x14, 0x0, %dg16                   ! t.c : 18
                    addd,2 0x0, 0x0, %dg17                    ! t.c : 18
                    addd,3 0x0, _f64,_lts2 0xbf847ae147ae147b, %dr7 ! t.c : 18
                    scld,4 0x5, 0x5, %dr12                    ! t.c : 14
                    return %ctpr3                             ! t.c : 14
                    setwd wsz = 0x7, nfx = 0x1, dbl = 0x0

0001<0000000105d0> :
                    addd,0 %dr0, _f16s,_lts1lo 0x80, %dr11    ! t.c : 18
                    addd,1 %dr0, _f16s,_lts1hi 0x60, %dr10    ! t.c : 18
                    addd,2 %dr0, _f32s,_lts2 0x40, %dr9       ! t.c : 18
                    addd,3 %dr0, _f32s,_lts3 0x20, %dr8       ! t.c : 18
                    disp %ctpr1, M_106b0                      ! t.c : 14
                    setwd wsz = 0x18, nfx = 0x1, dbl = 0x0
                    setbn rsz = 0x10, rbs = 0x7, rcur = 0x0

Таким простым способом удобно просматривать, какая часть кода ассемблера отвечает за конкретные строки программы.