8. Рекомендации по оптимизации программ под архитектуру Эльбрус¶
8.1. Рекомендации по работе со структурами данных¶
Работа с различными структурами данных может существенно отличаться по средней скорости доступа, темпу чтения и изменения. При выборе той или иной структуры данных может помочь нижеследующая информация о характеристиках таких структур:
простые одномерные массивы.
При регулярном считывании/записи элементов массива могут достигаться теоретически максимальные показатели темпа доступа к памяти - 32 байта в такт при попадании в
L2$
, 32 байта в 3 такта при гарантированном отсутствии вL2$
, при условии обращения к соседним элементам, причем для считывания может применяться механизм APB; при регулярном обращении с большим шагом (>64b) APB все еще применим, но темп существенно падает (до 64 раз при побайтовой обработке);одномерные массивы структур.
При регулярной обработке применим APB, однако, следует следить за тем, чтобы набор одновременно читаемых/записываемых полей в горячих участках был как можно более компактным; весьма полезен (в ущерб наглядности) переход от массивов структур к набору массивов, хранящих отдельные поля;
многомерные выстраиваемые массивы.
Многомерные массивы в языке Fortran (а также многомерные массивы в языке C при условии константных длин старших размерностей) являются одномерными по сути, но индексируемыми несколькими размерностями:
A(i,j,k) "FORTRAN" = a(i+j*dim1+k*dim1*dim2) "C"
Для повышения локальности нужно следить за тем, чтобы внутренняя размерность массивов (первая) индексировалась индуктивной переменной самого внутреннего цикла:
for i
for j
for k
A(k,j,i) // хорошо
B(i,j,k) // плохо
многомерные не выстраиваемые массивы.
Многомерные массивы в C являются массивами указателей на массивы (указателей на массивы и т.д. по размерностям); в связи с этим чтение одного элемента превращается в набор чтений с количеством, равным размерности; анализ зависимостей по адресам становится для компилятора весьма тяжелым;
списки.
Обход элементов списка представляет собой цикл с рекуррентностью (зависимостью между итерациями) вида
p=p->next
, иными словами, адрес, по которому производится чтение на текущей итерации, зависит от результата чтения на предыдущей итерации.При таком обходе темп перебора элементов списка не превышает 1 элемент на время доступа в
L1$
. Например, для процессора Е8С этот темп в 24 раза (!) меньше максимально возможного (при размере указателя 4 байта, и при условии, что все читаемые элементы расположены вL1$
). В случае, когда все операции чтения промахиваются и вL1$
, и вL2$
, темп падает до 1 элемента вt_latency
тактов; в связи с нерегулярностью адресов APB неприменим, но может быть эффективен механизмlist-prefetch
;деревья.
Деревья могут быть реализованы несколькими способами, но каждый из этих способов обладает тем же фундаментальным свойством, что и обычные списки: обход деревьев реализуется циклом с рекуррентностью по чтению из памяти, при этом, расположение перебираемых элементов дерева в памяти, как правило, еще хуже поддается упорядочению, чем множество перебираемых элементов списка;
хэш-таблицы.
Хэш-таблицы, как правило, строятся на базе обычных массивов, при этом чтение элемента хэш-таблицы предваряется вычислением хэш-функции, доступ становится нерегулярным, поэтому APB к перебору элементов хэша неприменим, тем не менее, возможна предварительная подкачка элементов хэша, считываемых на следующих итерациях.
8.2. Виды локальности данных¶
Виды локальности данных связаны с возможностями компилятора распознать зависимость между обращениями к этим данным. Семантика программ на императивных языках, таких как C и C++, строго последовательна; поэтому возможность распараллеливания вычислений зависит от способности компилятора обнаружить гарантированное отсутствие зависимостей между последовательностями обращения в память за данными.
Там, где логика программы не диктует необходимости определенной локальности данных, можно делать выбор в пользу одного из следующих типов локальности:
глобальные данные:
местоположение - сегмент
bss
кода;время жизни - вся программа;
адрес общедоступен;
не конфликтуют с другими данными (отсутствие конфликтов очевидно, если не было операций взятия адреса
&glob
);глобальные данные небольшого размера можно разместить на глобальных регистрах;
простые локальные данные без взятия адреса:
местоположение - регистры;
время жизни - до выхода из процедуры;
регистры не отображаются в память, ни с кем не конфликтуют;
сложные локальные данные, либо локальные данные со взятым адресом:
местоположение - пользовательский стек;
время жизни - до выхода из процедуры;
адрес доступен только внутри процедуры, для передачи данных в вызываемые процедуры нужно брать адрес;
в связи с часто необходимой операцией взятия адреса разрешение конфликтов по адресам становится более затруднительным;
динамические глобальные данные (
malloc
):местоположение - динамически выделяемая память;
время жизни - до динамического освобождения
free
;адрес доступен через указатели;
конфликты по адресам разрешимы с затруднениями, не разрешимы конфликты между разными экземплярами
malloc
в цикле;
динамические локальные данные (
alloca
):местоположение - пользовательский стек;
время жизни - до выхода из процедуры;
адрес доступен через указатели;
конфликты по адресам разрешимы с затруднениями, не разрешимы конфликты между разными экземплярами
malloc
в цикле.
8.3. Рекомендации по оптимизации процедур¶
Перед всякой оптимизацией необходимо получить общий профиль работы приложения или задачи. Основной интерес представляют процедуры с наибольшей долей исполнения в общем профиле. Без этого анализа вполне возможно добиться значительного ускорения отдельно взятых процедур, но итоговая производительность приложения существенно не улучшится.
8.3.1. Анализ процедуры: начальный этап¶
Для получения кода процедуры с профилем необходимо воспользоваться дизассемблером:
ldis -I m_program my_function1
В первую очередь необходимо определить тип анализируемой процедуры. Примерная классификация процедур с точки зрения анализа производительности выглядит следующим образом.
8.3.2. Короткая ациклическая процедура (не более 30 тактов)¶
Такие процедуры просты для анализа. Неоптимальность короткой процедуры как правило проявляется в виде:
плохой наполненности широких команд:
вследствие зацепления операций;
ввиду наличия длинных операций (деление, квадратный корень, вызов по косвенности);
вследствие конфликтов между чтениями/записями;
блокировки(ок) от операций чтения.
Общие рекомендации по исправлению найденных дефектов производительности:
инлайн-подстановка: собирать в режиме
-fwhole
, использовать двухфазную компиляцию-fprofile-generate
/-fprofile-use
;уменьшение длины зацепления;
принудительный разрыв конфликтов;
включение режима выноса чтений из процедур
-fipo-invup
;по возможности локализация данных для лучшего использования кэш-памяти.
8.3.3. Процедура с горячими простыми циклами/гнездами циклов¶
Анализ процедуры сводится к анализу работы горячих циклов. Наиболее частые проблемы:
плохая наполненность широких команд;
не применился механизм apb;
блокировки после операций чтения из-за промахов в кэш;
блокировки из-за превышения пропускной способности устройства памяти.
Предлагаемые пути решения означенных проблем:
малое число итераций может привести к отказу от применения конвейеризации (как следствие, к слабой наполненности широких команд), к отказу от использования механизма apb; если число итераций цикла объективно невелико (<5), следует рассмотреть возможность модификации алгоритма; если число итераций объективно велико, следует использовать двухфазную компиляцию
-fprofile-generate
/-fprofile-use
, либо добавить в исходный текст перед циклом подсказку:
#pragma loop count(100);
конвейеризированный цикл содержит длинную рекуррентность (длинно вычислимую зависимость между итерациями цикла). Рекомендуется проверить цикл на наличие рекуррентности, в случае нахождения — оценить ее целесообразность;
механизм apb не применяется из-за нерегулярного изменения адреса; рекомендуется использовать в качестве цикловых счетчиков, определяющих адрес чтения, переменные типа
long
(неunsigned
), не производить инкрементов счетчиков под условиями;механизм apb не применяется при невозможности статического определения выровненности чтений по размеру; рекомендуется пользоваться опцией
-faligned
(входит в состав-ffast
), подразумевающей выровненность адресов по размеру читаемого объекта;блокировки от операций чтения из-за кэш-промахов (
BUB_E0
): рекомендуется попробовать опции-fcache-opt
,-flist-prefetch
, включающие режим предварительной подкачки данных в кэш;блокировки по темпу работы памяти (
BUB_E2
): рекомендуется проверить темп обработки данных - сколько тактов работает цикл, сколько в нем операций чтения и записи, каков размер этих операций, какова локальность данных, какие данные могут быть найдены в кэше. Если темп существенно ниже ожидаемого, возможно, проблема в неравномерности использования ресурсов кэша второго уровня.
8.3.4. Сложный цикл с управлением, гнездо с управлением¶
Сложный цикл - цикл с управлением, несколькими обратными дугами.
Некоторые сложные циклы при наличии точной профильной информации (-fprofile-generate
/ -fprofile-use
)
могут быть сведены к простым применениям цикловых оптимизаций loop_nesting
, loop_unswitching
и некоторых других.
8.3.5. Громоздкая процедура¶
Громоздкие процедуры характеризуются неоднородным сложным управлением и размазанным профилем исполнения. Такие процедуры часто содержат циклы, как правило, с небольшим числом итераций, вызовы других процедур. Они сложны как для оптимизации компилятором, так и для анализа производительности, и здесь возможно дать только общие рекомендации:
если процедура стала громоздкой вследствие inline-подстановок других процедур, можно попробовать ограничить применение
inline
опциями;можно произвести вручную выделение важных фрагментов в отдельные процедуры.
8.3.6. Процедура с превалирующим оператором switch¶
Конструкции switch
с большим числом альтернатив, как правило, обрабатываются компилятором достаточно эффективно
при наличии адекватного профиля (см. опции -fprofile-generate
/ -fprofile-use
).
Если конструкция switch
имеет большое количество альтернатив с равномерно распределенной малой вероятностью,
тогда компилятор выразит её с помощью чтения из таблицы меток и косвенного перехода.
Более эффективно при этом работают конструкции switch
с плотным множеством значений,
т.к. в случае разреженного множества значений switch
таблица будет иметь большой размер.
8.3.7. Библиотечная процедура¶
Библиотечная процедура собирается один раз, но используется в разных контекстах с различными параметрами. Если производительность задачи определяется производительностью библиотечной процедуры, то может быть целесообразно спрофилировать важную функцию и пересобрать ее вместе с задачей.