.. _perf_recommend:

Рекомендации по оптимизации программ под архитектуру Эльбрус
============================================================

Рекомендации по работе со структурами данных
--------------------------------------------

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

* простые одномерные массивы.

  При регулярном считывании/записи элементов массива могут достигаться теоретически максимальные показатели темпа доступа к памяти
  - 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** к перебору элементов хэша неприменим, тем не менее,
  возможна предварительная подкачка элементов хэша, считываемых на следующих итерациях.

Виды локальности данных
-----------------------

Виды локальности данных связаны с возможностями компилятора распознать зависимость между обращениями
к этим данным. Семантика программ на императивных языках, таких как C и C++, строго последовательна;
поэтому возможность распараллеливания вычислений зависит от способности компилятора обнаружить
гарантированное отсутствие зависимостей между последовательностями обращения в память за данными.

Там, где логика программы не диктует необходимости определенной локальности данных, можно делать выбор в пользу
одного из следующих типов локальности:

* глобальные данные:

  * местоположение - сегмент ``bss`` кода;
  * время жизни - вся программа;
  * адрес общедоступен;
  * не конфликтуют с другими данными (отсутствие конфликтов очевидно, если не было операций взятия адреса ``&glob``);
  * глобальные данные небольшого размера можно разместить на глобальных регистрах;
  
* простые локальные данные без взятия адреса:

  * местоположение - регистры;
  * время жизни - до выхода из процедуры;
  * регистры не отображаются в память, ни с кем не конфликтуют;
  
* сложные локальные данные, либо локальные данные со взятым адресом:

  * местоположение - пользовательский стек;
  * время жизни - до выхода из процедуры;
  * адрес доступен только внутри процедуры, для передачи данных в вызываемые процедуры нужно брать адрес;
  * в связи с часто необходимой операцией взятия адреса разрешение конфликтов по адресам становится более затруднительным;
  
* динамические глобальные данные (``malloc``):

  * местоположение - динамически выделяемая память;
  * время жизни - до динамического освобождения ``free``;
  * адрес доступен через указатели;
  * конфликты по адресам разрешимы с затруднениями, не разрешимы конфликты между разными экземплярами ``malloc`` в цикле;
  
* динамические локальные данные (``alloca``):

  * местоположение - пользовательский стек;
  * время жизни - до выхода из процедуры;
  * адрес доступен через указатели;
  * конфликты по адресам разрешимы с затруднениями, не разрешимы конфликты между разными экземплярами ``malloc`` в цикле.
  
Рекомендации по оптимизации процедур
---------------------------------------------

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

Анализ процедуры: начальный этап
""""""""""""""""""""""""""""""""

Для получения кода процедуры с профилем необходимо воспользоваться дизассемблером:

.. code-block:: console

  ldis -I m_program my_function1

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

Короткая ациклическая процедура (не более 30 тактов)
"""""""""""""""""""""""""""""""""""""""""""""""""""""

Такие процедуры просты для анализа. Неоптимальность короткой процедуры как правило проявляется в виде:

* плохой наполненности широких команд:

  * вследствие зацепления операций;
  * ввиду наличия длинных операций (деление, квадратный корень, вызов по косвенности);
  * вследствие конфликтов между чтениями/записями;
* блокировки(ок) от операций чтения.

Общие рекомендации по исправлению найденных дефектов производительности:

* инлайн-подстановка: собирать в режиме :option:`-fwhole`, использовать двухфазную компиляцию :option:`-fprofile-generate` / :option:`-fprofile-use`;
* уменьшение длины зацепления;
* принудительный разрыв конфликтов;
* включение режима выноса чтений из процедур :option:`-fipo-invup`;
* по возможности локализация данных для лучшего использования кэш-памяти.

Процедура с горячими простыми циклами/гнездами циклов
"""""""""""""""""""""""""""""""""""""""""""""""""""""

Анализ процедуры сводится к анализу работы горячих циклов. Наиболее частые проблемы:

* плохая наполненность широких команд;
* не применился механизм **apb**;
* блокировки после операций чтения из-за промахов в кэш;
* блокировки из-за превышения пропускной способности устройства памяти.

Предлагаемые пути решения означенных проблем:

* малое число итераций может привести к отказу от применения конвейеризации (как следствие, к слабой наполненности широких команд),
  к отказу от использования механизма **apb**; если число итераций цикла объективно невелико (<5),
  следует рассмотреть возможность модификации алгоритма;
  если число итераций объективно велико, следует использовать двухфазную компиляцию :option:`-fprofile-generate` / :option:`-fprofile-use`,
  либо добавить в исходный текст перед циклом подсказку:

.. code-block:: c

  #pragma loop count(100);
  
* конвейеризированный цикл содержит длинную рекуррентность (длинно вычислимую зависимость между итерациями цикла).
  Рекомендуется проверить цикл на наличие рекуррентности, в случае нахождения — оценить ее целесообразность;
* механизм **apb** не применяется из-за нерегулярного изменения адреса;
  рекомендуется использовать в качестве цикловых счетчиков, определяющих адрес чтения, переменные типа ``long`` (не ``unsigned``),
  не производить инкрементов счетчиков под условиями;
* механизм **apb** не применяется при невозможности статического определения выровненности чтений по размеру;
  рекомендуется пользоваться опцией :option:`-faligned` (входит в состав :option:`-ffast`), подразумевающей выровненность адресов по размеру читаемого объекта;
* блокировки от операций чтения из-за кэш-промахов (``BUB_E0``):
  рекомендуется попробовать опции :option:`-fcache-opt`, :option:`-flist-prefetch`, включающие режим предварительной подкачки данных в кэш;
* блокировки по темпу работы памяти (``BUB_E2``): рекомендуется проверить темп обработки данных -
  сколько тактов работает цикл, сколько в нем операций чтения и записи, каков размер этих операций, какова локальность данных,
  какие данные могут быть найдены в кэше.
  Если темп существенно ниже ожидаемого, возможно, проблема в неравномерности использования ресурсов кэша второго уровня.

Сложный цикл с управлением, гнездо с управлением
""""""""""""""""""""""""""""""""""""""""""""""""

Сложный цикл - цикл с управлением, несколькими обратными дугами.
Некоторые сложные циклы при наличии точной профильной информации (:option:`-fprofile-generate` / :option:`-fprofile-use`)
могут быть сведены к простым применениям цикловых оптимизаций ``loop_nesting``, ``loop_unswitching`` и некоторых других.

Громоздкая процедура
""""""""""""""""""""

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

* если процедура стала громоздкой вследствие inline-подстановок других процедур, можно попробовать ограничить применение ``inline`` опциями;
* можно произвести вручную выделение важных фрагментов в отдельные процедуры.

Процедура с превалирующим оператором switch
"""""""""""""""""""""""""""""""""""""""""""

Конструкции ``switch`` с большим числом альтернатив, как правило, обрабатываются компилятором достаточно эффективно
при наличии адекватного профиля (см. опции :option:`-fprofile-generate` / :option:`-fprofile-use`).
Если конструкция ``switch`` имеет большое количество альтернатив с равномерно распределенной малой вероятностью,
тогда компилятор выразит её с помощью чтения из таблицы меток и косвенного перехода.
Более эффективно при этом работают конструкции ``switch`` с плотным множеством значений,
т.к. в случае разреженного множества значений ``switch`` таблица будет иметь большой размер.

Библиотечная процедура
""""""""""""""""""""""

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

..
 Такой режим компиляции включается по опциям :option:`-fprofile-generate-ext`, :option:`-fprofile-use-ext`.
