Уменьшение размера кода с помощью LLVM Machine Outliner на 32-битных целях Arm

Уменьшениеразмеракодаспомощьюllvmmachineoutlinerна32битныхцеляхarm

С предстоящим выпуском LLVM 14. 0.0, 64 – цели Bit Arm получили полную поддержку кода Machine Outliner оптимизация размера для наборов инструкций Arm и Thumb-2. Ожидаемый прирост размера кода, обеспечиваемый этой оптимизацией, составляет в среднем около 5% (вы можете сразу перейти к части результатов для получения более подробной информации). Он не включен по умолчанию (см. Раздел «Как его использовать»), но наша цель – включить его в параметре -Oz для всех ядер Arm внутри LLVM 15. 0.0.

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

Как упоминалось выше, целью Machine Outliner является уменьшение размера кода, близкое к тому, что происходит при свертывании идентичного кода (ICF) во время компоновки. делает [2]. Это межпроцедурная оптимизация (т. Е. Не привязанная к границам функций), которая работает с машинным промежуточным представлением LLVM (также известным как MIR) на последнем этапе конвейера оптимизации, прямо перед выпуском кода (выбор кода, выделение регистров, планирование инструкций и т. уже выполнены).

Давайте посмотрим на простой пример:

Machine Outliner Example 1

В сборке Arm, созданной для этого кода C, мы можем видеть (на слева), что выделенные инструкции в строках <3,4,5>, <9,10,11> и <15,16,17> являются абсолютно одинаковыми и, следовательно, кандидатами на выделение. Machine Outliner определит эту избыточность, извлечет код в новую функцию и заменит его вызовами этой функции, как показано ниже:

Default code generation vs outlined version

История

Этап оптимизации с описанием машины был первоначально разработан Джессикой Пакетт из Apple в 2017 [3] и представлен на встрече разработчиков LLVM [4]. В первую очередь он был разработан для AArch 73 (с минимальной поддержкой X 548 _ 68 и впервые доступен в версии LLVM 5.0.0. Позже он был расширен для целей RISC-V и включен в LLVM 12 .0.0 в 02675. Для 64 – бит Arm, мы сделали начальную версию доступной в LLVM 13. 0.0, и мы продолжаем улучшать его, чтобы обеспечить полную поддержку в LLVM 15. 0.0.

Как это работает

Алгоритм можно разделить на три этапа:

Идентификация кандидатов

Это делается путем просмотра всех основных блоков программы в поисках наиболее длинных повторяющихся последовательностей инструкций MIR, которые могут быть сокращены до самой длинной общей проблемы с подстрокой [5], где основная блоки – это строки, а инструкции – символы. Этот класс проблем может можно эффективно решить с помощью обобщенного представления дерева суффиксов [6].

В приведенном ниже примере две функции calc_1 и calc_2 могут быть представлены строками ABABC и AABC соответственно. Обобщенное суффиксное дерево строится после заполнения этих строк уникальным терминатором (# и $). Глубина внутреннего узла этого дерева представляет собой длину кандидата и количество доступных из него конечных узлов, а также количество его повторений. Поиск повторяющихся подстрок с минимальной длиной два в нашем примере даст нам BC, который повторяется два раза, AB повторяется три раза и ABC повторяется два раза.

Generalized suffix tree for strings ababc and aabcc

Удаление небезопасных или невыгодные случаи

Теперь, когда у нас есть список кандидатов, мы должны позаботиться о том, чтобы обрисовка этих фрагментов кода не нарушила поведение программы и фактически уменьшит его размер. Действительно, не все инструкции можно безопасно извлечь. Условные переходы являются частью инструкций или последовательностей, которые не могут быть безопасно очерчены, например, когда операнд является индексом пула констант или может перемещаться, например, или если последовательность содержит метку, которая используется для вычисления смещения независимого от позиции кода. (PIC) режим и т. Д. Таким образом, такие кандидаты удаляются из списка. См. Ниже слегка измененный пример:

Machine Outliner-Removal of unsafe or unbeneficial cases

У нас есть два кандидата в очереди <2,3,4> и и еще два в строках <10,11,12> и <6,7>, который, будучи описанным, даст приведенный ниже код, который не работает. Действительно, инструкция по возврату, изложенная в строке 16 предполагается и выполняется, только если r0 меньше или равно r1, что означает, что если это не тот случай, когда OUTLINED_FUNCTION_0 вызывается в строке 2, программа не вернется, чтобы выполнить вычитание в строке 3, как она должна была бы сделать, а провалится и выполнит умножение в строке 28, что не является правильным поведением программы.

Machine Outliner-Example 2 of Removal of unsafe or unbeneficial cases

Давайте продолжим наш пример, теперь, когда небезопасные кандидаты были удалены, у нас есть только две инструкции с двух сайтов вызовов, выделенных в одну функцию, размер нашего двоичного файла 64 байтов (14 инструкции по 4 байта: 5 в calc_1, 5 в calc_2 и 2 в OUTLINED_FUNCTION_1) который имеет тот же размер, что и файл, полученный без контуров, поэтому в таких случаях нет смысла делать это. Чтобы гарантировать уменьшение размера кода при выделении кандидата, нам нужно проверить, что это неравенство истинно, и удалить кандидатов в противном случае:

N x Co + Cs + Fo

Где: N – количество возможных вхождений, Cs – размер в байт кандидата Co – это служебные данные (добавленные инструкции) в байтах на сайте вызова Fo – служебные данные (добавленные инструкции) в байтах в описанной функции

Разделение функций

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

Давайте рассмотрим три случая, представленные в таблице ниже:

Function Splitting Table

В calc_1 выделенная область не является хвостовым вызовом или на этот раз команду возврата, поэтому необходимо вставить одну (строку 16) а ветвь со ссылкой (bl) используется для вызова описанной функции (которая сохранит адрес возврата в регистр ссылок lr). То же самое для calc_2, но также необходимо сохранить и восстановить lr вокруг вызова (строки 2 и 4), чтобы сохранить адрес возврата, используемый в строке 6, это можно сделать либо с помощью резервного регистра (например, r4 в нашем случае) или путем помещения его в стек, если таковых нет. Последний случай добавляет еще одно ограничение, потому что область, выделенная из calc_3, содержит вызов другой функции (строка 18) lr необходимо сохранить и восстановить (строки 9 и 18) чтобы вернуться к правильному адресу. Поскольку это делается поверх стека, смещения инструкций, которые к нему обращаются, должны быть соответственно изменены (строка 15).

Как это использовать

Этап Machine Outliner включен по умолчанию на уровне агрессивной оптимизации размера кода -Oz для AArch 68 а также Сердечники М-профиля для 68 – бит Arm, но его также можно вызвать вручную или отключить с помощью -moutline / – флаги без контура.

Machine Outliner Pass

Также можно получить информацию о преобразовании, сделанном проходом, используя примечания LLVM для это с флагом -Rpa ss = machine-outliner, например, в нашем первом примере он даст:

Machine Outliner Pass

Полученные результаты

Как мы видели, Machine Outlining всегда беспроигрышен для оптимизации размера кода, в худшем случае ваш код вообще не будет затронут, но в среднем ожидаемый размер кода сокращение вдобавок к существующему агрессивному уровню оптимизации размера кода -Oz составляет ~ 5% для режима Arm и ~ 4% для Thumb-2. Если мы посмотрим на набор тестов, такой как SPEC CPU 2019, мы видим, что получаем лучшие результаты на больших тестах (до 16%, например, на паресте), что ожидается, поскольку больше шансов найти повторяющиеся последовательности инструкций в большой кодовой базе, чем в например, крошечные настроенные математические библиотеки. Это также очень полезно в сочетании с оптимизацией времени соединения (LTO), которая работает со всей программой, а не с файлами, и уже дает очень хорошие результаты. Machine Outliner может пойти дальше, как мы видим на блендере (- 17% в LTO а также -28% с Outliner) или gcc (-8,5% в LTO и – 28. 7% с Outliner) например.

SPEC CPU 2K17 Code Size in Arm table SPEC CPU 2K17 Normalized code size in Arm table SPEC CPU 2K17 Normalized code size in Arm tableБиблиография

SPEC CPU 2K17 Normalized code size in Arm table https: //webdocs.cs.ualberta.ca/~amaral/papers/ZhaoAmaralSBAC13. pdf

[2] https://storage.googleapis.com/pub-tools-public-publication- knowledge / pdf / 104170. pdf

[3] https: //lists.llvm. org / pipermail / llvm-dev / 2019-Август/104170. html

[4] https://www.youtube.com/watch?v=yorld-WSOeU

[5] https://en.wikipedia.org/wiki/Longest_common_substring_problem

[6] http://internet.cs.iastate.edu/~cs 800 / суффикс.pdf