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. Просмотр привязки к исходнику¶
Тестовый пример умножения векторов:
//mul_vector_eml.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#include <eml/eml.h>
#define N 100000
#define M 1000000
int main()
{
int i,j;
double s=0.0;
double *A;
double *B;
double *C;
A = (double*)malloc(N * sizeof(double));
B = (double*)malloc(N * sizeof(double));
C = (double*)malloc(N * sizeof(double));
srand(time(NULL));
for (i = 0; i < N; i++)
{
A[i] = (double)(rand()%10000)/(double)10000+(double)(rand()%10000);
B[i] = (double)(rand()%10000)/(double)10000+(double)(rand()%10000);
}
for (j=0; j<M; j++)
eml_Vector_Mul_64F(A,B,C,N);
s+=C[0];
free(A);
free(B);
free(C);
return (int)s;
}
Чтобы посмотреть привязку в ассемблерном файле, нужно использовать дополнительную опцию -fverbose-asm:
$ lcc mul_vector_eml.c -fverbose-asm -S -O3
$ cat mul_vector_eml.s
.file "mul_vector_eml.c"
.ignore ld_st_style
.ignore strict_delay
! -----------------------------------------------------------------------------
.text
! t.c : 11
.global main
.type main, #function
.align 8
main:
! <0000>
{
setwd wsz = 0xc, nfx = 0x1, dbl = 0x0 ! t.c : 11
setbn rsz = 0x3, rbs = 0x8, rcur = 0x0 ! t.c : 11
disp %ctpr2, malloc ! t.c : 17
getsp,0 _f32s,_lts1 0xffffffe0, %dr2 ! t.c : 11
addd,1 0x0, _f32s,_lts2 0xc3500, %dr11 ! t.c : 17
adds,2 0x0, _f32s,_lts3 0x68db8bad, %r9 ! t.c : 24
addd,3 0x1f, 0x1, %dr7 ! t.c : 24
adds,4 0x1f, 0x0, %r8 ! t.c : 24
addd,5,sm 0x0, 0x0, %dr0 ! t.c : 22
}
! <0001>
{
nop 3
addd,0,sm 0x0, %dr11, %db[0] ! t.c : 17
addd,1 0x0, _f64,_lts0 0x40c3880000000000, %dr10 ! t.c : 24
adds,2 0x0, 0x0, %r6 ! t.c : 22
}
...
Перед каждой функцией будет помещена строка с привязкой к исходнику, а затем на каждой команде, где известна привязка к исходнику, будет напечатано имя файла и номер строки. Чтобы удобнее было искать соответствие между ассемблером и дизассемблером, для архитектуры e2k дополнительно в угловых скобках будет напечатан номер такта.
Чтобы посмотреть привязку в кодовском файле, нужно использовать дополнительную опцию -gline:
$ lcc -O3 mul_vector_eml.c -gline -c
$ ldis mul_vector_eml.o
! function 'main', entry = 9, value = 0x000000, size = 0x420, sect = ELF_TEXT num = 1
0000<000000000000> main:
getsp,0 _f32s,_lts1 0xffffffe0, %dr2 ! t.c : 11
addd,1 0x0, _f32s,_lts2 0xc3500, %dr11 ! t.c : 17
adds,2 0x0, _f32s,_lts3 0x68db8bad, %r9 ! t.c : 24
addd,3 0x1f, 0x1, %dr7 ! t.c : 24
adds,4 0x1f, 0x0, %r8 ! t.c : 24
addd,5,sm 0x0, 0x0, %dr0 ! t.c : 22
disp %ctpr2, $malloc ! t.c : 17
setwd wsz = 0xc, nfx = 0x1, dbl = 0x0
setbn rsz = 0x3, rbs = 0x8, rcur = 0x0
0001<000000000038> :nop 3
addd,0,sm 0x0, %dr11, %db[0] ! t.c : 17
addd,1 0x0, _f64,_lts0 0x40c3880000000000, %dr10 ! t.c : 24
adds,2 0x0, 0x0, %r6 ! t.c : 22
...
В каждой команде, для которой имеется соответствие, будет напечатано имя файла и номер строки. Таким простым способом удобно просматривать, какая часть кода отвечает за конкретные строки программы.
В компиляторе информация про привязки к исходникам находится здесь:
/opt/mcst/doc/code_to_source.html
Использование библиотеки eml подробно описано в разделе Использование оптимизированных библиотек