\makeReportTitle{лабораторной}{2}{Разработка программного обеспечения. \\Отладка и профилирование кода}{Программное обеспечение встроенных систем}{}{доцент кафедры ИУ-3 \\ Федоров С.В.}
\newpage
\pagestyle{fancy}
%\blankpage
\tableofcontents
\newpage
\section*{Цель работы}
\addcontentsline{toc}{section}{Цель работы}
В лабораторной работе необходимо осуществить отладку программы, её профилирование, выполнить требования по быстродействию и объему для отдельных функций ПО. Изучить влияние ручных оптимизаций циклов на быстродействие программы. Изучить оптимизации компилятора на уровне формируемого ассемблерного кода.
\section*{Задания}
\addcontentsline{toc}{section}{Задания}
\begin{itemize}
\item Общее задание части I
\begin{enumerate}
\item Исходный код программы обработки прерывания от кнопок с замером времени выполнения обработчика прерывания.
\item Результаты измерения времени выполнения первого и последующих вызовов обработчика прерывания для разных уровней оптимизации.
\item Прокомментированный ассемблерный код обработчика прерывания для оптимизации Level3 и для отключенной оптимизации.
\item Сравнение результатов и выводы.
\end{enumerate}
\item Индивидуальное задание части I
\begin{enumerate}
\item Текст индивидуального задания.
\item Исходный код программы, в которой реализована инициализация и обработка массива данных с помощью двух разработанных по заданию функций: одна без развертывания цикла, а другая с развертыванием цикла.
\item Результаты измерения времени выполнения разработанных функций для оптимизации Level3 и для отключенной оптимизации.
\item Результаты измерения количества тактов, затрачиваемых на одну итерацию неразвернутого цикла при включенной оптимизации Level3 при разных объемах массивов данных.
\item Прокомментированный ассемблерный код разработанных функций для оптимизации Level3 и для отключенной оптимизации.
\item Сравнение результатов и выводы.
\end{enumerate}
\item Общее задание части II
\end{itemize}
\section*{Общее задание части I.}
\addcontentsline{toc}{section}{Общее задание части I.}
Для подсчёта времени был модифицирован код из первой лабораторной работы (добавлен таймер и вывод информации с него). Код представлен в листинге \hrf{lst:btnISR} внесённые изменения отмечены \lh{red}{красным}, функции, директивы и комментарии, оставшиеся без изменений опущены:
\begin{lstlisting}[language=C, style=CCodeStyle, caption=Performance counter в обработчике прерываний, label={lst:btnISR}]
Для помещения функции обработчика прерываний в тесно связанную память был добавлен прототип функции с указанием атрибута, уточняющего секцию памяти, в которой функция должна располагаться.
В результате выполнения данного кода были получены результаты, представленные в таблице \hrf{table:isrtime}. Скриншоты с этими же результатами представлены в приложении \hrf{appendix:isrtime}. После нескольких запусков с разными вариантами настроек можно однозначно сказать, что при расположении обработчика прерываний в тесносвязанной памяти очевидно ускорение работы функции даже без оптимизаций компилятора. Из результатов видно, что размещённый в тесносвязанной памяти код выполняется так, будто находится в кэше. При размещении в обычной памяти, первый вызов выполняется дольше, поскольку обработчик перемещается в кэш (далее он оттуда не вытесняется, поскольку в целом программа очень простая). Время работы при разных типах оптимизации не меняется так как обработчик очень простой.
\begin{table}[H]
\centering
\begin{tabular}{||l|l|c|c||}
\hline
Использование TCM & Оптимизация & Количество прерываний & Время (тактов) \\ [0.5ex]
\hline\hline
нет & нет & 1 & 62 \\
нет & нет & 10 & 314 \\
нет & -О2 & 1 & 43 \\
нет & -О2 & 10 & 223 \\
нет & размер & 1 & 43 \\
нет & размер & 10 & 223 \\
да & нет & 1 & 28 \\
да & нет & 10 & 280 \\
да & -О3 & 1 & 20 \\
да & -О3 & 10 & 200 \\
да & размер & 1 & 20 \\
да & размер & 10 & 200 \\ [1ex]
\hline
\end{tabular}
\caption{Зависимость времени исполнения функции от количества прерываний}
\label{table:isrtime}
\end{table}
Оптимизации процессора значительно повлияли на транслируемый код, скопмилировав в результате ассемблерный код, представленный в листингах \hrf{lst:noopt} и \hrf{lst:optlv2}. В неоптимизированном коде видно гораздо больше обращений к памяти (мнемоники \code{stw}, \code{ldw}), в то время, как оптимизированный код сразу работает только с периферийным модулем (мнемоники \code{stwio}, \code{ldwio}). Также неоптимизированная версия программы сразу выделяет место для своей работы на стеке (\code{sp,sp,-16}), сохраняет принятые в функцию параметры, несмотря на то, что один из них не используется вовсе, а второй используется только один раз (\code{r4}, \code{r5} записываются в -8 и -12 сдвиги от указателя на фрейм, соответственно, а читается только \code{r4}: \code{ldw r2,-8(fp)}) и активнее работает с локальными регистрами, сохраняя в них промежуточные значения, прочитанные из периферийных модулей. В то время, как оптимизированная функция работает с единственным, возвращающим, регистром \code{r2}.
\begin{lstlisting}[style=ASMStyle, caption=Код без оптимизаций, label={lst:noopt}]
00000284 <handle_button_interrupts>:
284: defffc04 addi sp,sp,-16
288: df000315 stw fp,12(sp)
28c: df000304 addi fp,sp,12
290: e13ffe15 stw r4,-8(fp)
294: e17ffd15 stw r5,-12(fp)
298: 0007883a mov r3,zero
29c: 00820034 movhi r2,2048
2a0: 10c40535 stwio r3,4116(r2)
2a4: e0bffe17 ldw r2,-8(fp)
2a8: e0bfff15 stw r2,-4(fp)
2ac: 00820034 movhi r2,2048
2b0: 10c42337 ldwio r3,4236(r2)
2b4: e0bfff17 ldw r2,-4(fp)
2b8: 10c00015 stw r3,0(r2)
2bc: 0007883a mov r3,zero
2c0: 00820034 movhi r2,2048
2c4: 10c42335 stwio r3,4236(r2)
2c8: 00820034 movhi r2,2048
2cc: 10842337 ldwio r2,4236(r2)
2d0: 0007883a mov r3,zero
2d4: 00820034 movhi r2,2048
2d8: 10c40435 stwio r3,4112(r2)
2dc: 0001883a nop
2e0: e037883a mov sp,fp
2e4: df000017 ldw fp,0(sp)
2e8: dec00104 addi sp,sp,4
2ec: f800283a ret
\end{lstlisting}
\begin{lstlisting}[style=ASMStyle, caption=Код с оптимизацией -О2, label={lst:optlv2}]
00000284 <handle_button_interrupts>:
284: 00820034 movhi r2,2048
288: 10040535 stwio zero,4116(r2)
28c: 10842337 ldwio r2,4236(r2)
290: 20800015 stw r2,0(r4)
294: 00820034 movhi r2,2048
298: 10042335 stwio zero,4236(r2)
29c: 10842337 ldwio r2,4236(r2)
2a0: 00820034 movhi r2,2048
2a4: 10040435 stwio zero,4112(r2)
2a8: f800283a ret
\end{lstlisting}
\section*{Индивидуальное задание части I.}
\addcontentsline{toc}{section}{Индивидуальное задание части I.}
\subsection*{Развёрнутый цикл и оптимизации}
Индивидуальным заданием в первой части работы был расчёт контрольной суммы UDP для блока данных. Код программы, реализующей вызов функции расчёта контрольной суммы и подсчёт времени выполнения посредством Performance Counter представлен в листинге \hrf{lst:udpCode}. На строках \hrf{line:nounroll} и \hrf{line:unroll} представлены циклы, выполняющие одну и ту же задачу, но без применения развёртывания цикла (loop unrolling) и с применением, соответственно. Также важно, что цикл без развёртывания является циклом <<добора>>, то есть когда развёрнутый цикл (строка \hrf{line:unroll}) завершает свою работу в массиве данных могут остаться некратные данные, которые досчитываются циклом со строки \hrf{line:nounroll}.
unsigned short checkSum(unsigned short* addr, int size) __attribute__ ((section("irq")));
unsigned short checkSum(unsigned short* addr, int size) {
register unsigned short checksum = 0;
register int sum = 0;
// unrolled cycle
while (size > 8) { <@\label{line:unroll}@>
sum += *addr++;
sum += *addr++;
sum += *addr++;
sum += *addr++;
size -= 8;
}
// simple cycle (and)
while (size > 1) { <@\label{line:nounroll}@>
sum += *addr++;
size -= 2;
}
if (size > 0) {
sum += *(unsigned char *) addr;
}
while (sum >> 16) {
sum = (sum & 0xffff) + (sum >> 16);
}
checksum = ~sum;
return checksum;
}
int main(void) {
printf("udp tcm 3 opt unroll cycle\n");
PERF_RESET(PERFORMANCE_COUNTER_0_BASE);
const int SIZE = CACHE_CHECK;
unsigned short udpCheckSum = 0;
short datagram[SIZE];
const void *buf;
// filling datagram
short c1 = 0xf001;
short c2 = 0x0e11;
short c3 = 0xaad0;
short c4 = 0xa00c;
int i = 0;
do {
datagram[i + 0] = c1;
datagram[i + 1] = c2;
datagram[i + 2] = c3;
datagram[i + 3] = c4;
i += 4;
} while (i < SIZE);
buf = (const void*) datagram;
PERF_START_MEASURING(PERFORMANCE_COUNTER_0_BASE);
i = 100;
while (0 < --i) {
PERF_BEGIN (PERFORMANCE_COUNTER_0_BASE, 1);
udpCheckSum = checkSum(buf, (SIZE + 1) * 2);
PERF_END (PERFORMANCE_COUNTER_0_BASE, 1);
}
perf_print_formatted_report (
PERFORMANCE_COUNTER_0_BASE,
ALT_CPU_FREQ, 1,
"UDP 2048",
"Time");
PERF_STOP_MEASURING (PERFORMANCE_COUNTER_0_BASE);
printf("checksum = 0x%x\n", udpCheckSum);
return 0;
}
\end{lstlisting}
Результаты замеров времени выполнения этих двух вариантов функции приведены в таблице \hrf{table:udpchecksum}. Все измерения проводились на 99ти повторениях, снимки экрана представлены в приложении \hrf{appendix:checksumTime}.
\begin{table}[H]
\centering
\begin{tabular}{||r|c|c||}
\hline
Оптимизация & Цикл развёрнут & Время(тактов) \\ [0.5ex]
\hline\hline
нет & нет & 24322152 \\
нет & да & 14470051 \\
-О3 & нет & 6835626 \\
-О3 & да & 4174875 \\
\hline
\end{tabular}
\caption{Применение развёртывания цикла при выполнении вычислений}
\label{table:udpchecksum}
\end{table}
Для массивов длиной 8192 элементов типа \code{unsigned short}, циклически заполненных значениями \code{0xf001}, \code{0x0e11}, \code{0xaad0}, \code{0xa00c} получилось значение \code{0xbec8}, что является корректным значением контрольной суммы.
В процессе выполнения программы были произведены измерения количества тактов, затрачиваемых на одну итерацию неразвернутого цикла при включенной оптимизации -O3 при разных объемах массивов данных. Результаты измерений приведены в таблице \hrf{table:memory}. Подсчёт результатов был произведён без изменения положения Performace Counter (для всей функции целиком) согласно средней асимтотической сложности алгоритма получения контрольной суммы O(n), где n -- это размер входящего массива данных. Для системы было выделено 16384 байта, соответственно, для заполнения её согласно задания необходимо создать массивы на 16384 ($200\%$), 8192 ($100\%$), 4096 ($50\%$), 2048 ($25\%$) элементов типа \code{unsigned short} (шестнадцатиразрядные беззнаковые целые числа). Снимки экрана с результатами работы Performance counter представлены в приложении \hrf{appendix:udpMemory}.
Цикл развёрнут &\thead{Расчётное\\заполнение памяти\\(процентов)}&\thead{Время\\функции\\(секунд)}&\thead{Расчётное время\\одной итерации\\(мксек)}\\ [0.5ex]
\hline\hline
нет & 25 & 0.03427 & 0.169024 \\
нет & 50 & 0.06843 & 0.168753 \\
нет & 100 & 0.13671 & 0.168568 \\
нет & 200 & 0.27328 & 0.168482 \\
да & 25 & 0.02097 & 0.413707 \\
да & 50 & 0.04181 & 0.412425 \\
да & 100 & 0.08350 & 0.411833 \\
да & 200 & 0.16676 & 0.411241 \\
\hline
\end{tabular}
\caption{Скорость исполнения одной итерации относительно заполнения памяти}
\label{table:memory}
\end{table}
В таблице видно, что каждая итерация развёрнутого цикла выполняется дольше, но не в четыре раза, поэтому за счёт уменьшения количества итераций в четыре раза получается довольно значительный прирост в скорости исполнения программы. Такой прирост обусловлен снижением накладных расходов на каждую итерацию цикла. В листингах \hrf{lst:nooptsim} и \hrf{lst:nooptunf} представлен ассемблерный код функции подсчёта контрольной суммы без применения развёртывания основного цикла и с применением, соответственно, а в листингах \hrf{lst:optsim} и \hrf{lst:optunf} такой же код, но с оптимизацией компилятора -О3.
В листинге \hrf{lst:nooptsim} на строках с\hrf{line:simbody} по \hrf{line:simcond} виден цикл, обрабатывающий значения из массива. Аналогичный цикл виден как <<цикл добора>> в листинге \hrf{lst:nooptunf} на строках с\hrf{line:unfsimbody} по \hrf{line:unfsimcond}. Адреса начала и конца тел циклов выделены \lh{blue}{синим}.
В листинге с развёртыванием цикла на каждой итерации осуществляется четыре идентичных действия по чтению значения из исходного массива (\hrf{line:unfit1b}, \hrf{line:unfit2b}, \hrf{line:unfit3b}, \hrf{line:unfit4b}), выделены \lh{dkgreen}{зелёным}; прибавления этого значения в регистр суммы и инкремент указателя на массив (\hrf{line:unfit1e}, \hrf{line:unfit2e}, \hrf{line:unfit3e}, \hrf{line:unfit4e}), выделены \lh{red}{красным}. Далее в обеих функциях следует условие добора нечётности исходного массива и цикл преобразования суммы к 16-разрядному значению.
\begin{lstlisting}[style=ASMStyle, caption=Код без развёртывания, label={lst:nooptsim}]
// <@\lh{dkgreen}{выделить для стека функции 16 байт}@>
10000000: defffc04 addi sp,sp,-16
// <@\lh{dkgreen}{сохранить в указатель на фрейм 12й байт (первый элемент) стека}@>
10000004: df000315 stw fp,12(sp)
// <@\lh{dkgreen}{что в sp(8)?}@>
10000008: dc000215 stw r16,8(sp)
// <@\lh{dkgreen}{fp = sp+12}@>
1000000c: df000304 addi fp,sp,12
// <@\lh{dkgreen}{указатель на переданный массив в }@>fp-8
10000010: e13ffe15 stw r4,-8(fp)
// <@\lh{dkgreen}{переданная длина массива в }@>fp-12
В оптимизированной версии кода в листингах \hrf{lst:optsim} и \hrf{lst:optunf} также есть общая часть -- условие добора нечётности массива и цикл преобразования к 16-разрядному значению, начало и конец выделены \lh{red}{красным}.
В листинге \hrf{lst:optunf} развёрнутый цикл, описанный явно выделен \lh{dkgreen}{зелёным}. В развёрнутом цикле видно обращение по адресу массива со смещением 0, 2, 4 и 6 байт, соответственно. Основной цикл (для задания с явным развёртыванием цикла - это <<цикл добора>>) выделен в листингах \lh{blue}{синим}, причём в листинге \hrf{lst:optunf} очевидна развёртывающая оптимизация компилятора на строках \hrf{line:unfoptldw0}, \hrf{line:unfoptldw2}, \hrf{line:unfoptldw4}, \hrf{line:unfoptldw6} (выделены \lh{blue}{синим}) следует загрузка значений из массива с последующим сдвигом указателя на исходный массив и суммированием прочитанных значений с промежуточным значением суммы (регистр \code{r2}), дополнительные комментарии представлены в листинге.
Дополнительно были отслежены изменения вспомогательных регистров (для мгновенного расчёта адреса чтения из массива по окончании работы развёрнутого цикла)
\begin{lstlisting}[language=C,style=CCodeStyle]
r4=array& 1000
r5=size 100
00 r2=r5<9 false
04 r2==0
08 r9=20-9 91
0c r9>>=3 11
10 r2=0 0
14 r8=r9+1 12
18 r8<<=3 96
1c r8+=r4 1096
20 c=================
24 c
28 l
2c e
30
34 u
38 n
3c f
40 l
44 d=================
48 r9*=-8 -88
4c r5-=8 92
50 r5+=r9 4
\end{lstlisting}
\section*{Часть II.}
\addcontentsline{toc}{section}{Часть II.}
\subsection*{Задание}
Во второй части работы требовалось изучить отчёт профилировщика, сохранить иерархию вызовов функции main и начало таблицы <<плоского>> профиля. На примере реализованной функции обработки данных в части I определить с помощью Performance Counter, какой оверхед (накладные расходы) в циклах вносит вызов функции \code{mcount} в выполнение функции. Изучите ассемблерный код и найдите в нем вызов функции \code{mcount}. Результаты сохраните в отчет.
Согласно показаний Performance Counter (рис. \hrf{pic:pcpt2}) функция подсчёта контрольной суммы без подключенного профайлера выполнялась в среднем 843.434 микросекунд, ас вызовом \code{mcount} 863.816, то есть на $\approx20$ мкс больше.
\caption{Показания Performance Counter для вызовов функции со включенным профилировщиком}
\label{pic:pcpt2}
\end{figure}
В листинге \hrf{lst:gprofrpt} приведены первые строки плоского профиля выполнения программы, демонстрирующие, что программа находилась 93\% времени в функции \code{main}, то есть, выполняла работу основного цикла (прямые вызовы функции подсчёта контрольной суммы).
В листинге \hrf{lst:gprofassemble} отмечен \lh{red}{красным} вызов функции \code{mcount}. Интересно, что адрес функции (\code{0000ddfc <_mcount>:}) считается налету с применением временного регистра.