+7 (812) 670-9095
Обратная связьEnglish
Главная → Статьи → Системное ПО → Оптимизированный адаптируемый компилятор для встраиваемых процессоров: GCC против LLVM
Полезный совет
БП 3.0 Такси: Как получить быстрый доступ к документам, справочникам и настройкам программы? Подробнее
Версия для печати

Оптимизированный адаптируемый компилятор для встраиваемых процессоров: GCC против LLVM

5 октября 2017

АстроСофт занимается компиляторами с 1999 года. В нашем активе — собственный универсальный компилятор UCC, позволяющий быстро создавать компиляторы под целевые платформы заказчика. При этом мы разрабатываем и остальные компоненты SDK: ассемблер, линкер, отладчик и др.

В 2017 году в АстроСофт стартовало несколько проектов по разработке полномасштабных LLVM-компиляторов, и наши эксперты подготовили перевод статей, посвященных LLVM. Представляем вторую статью из этого цикла.

Авторы: Лавиния Гика , Николае Тапус.




Адаптируемые компиляторы приобретают все большую популярность, так как они используются даже на стадии проектирования процессоров. Сокращение срока вывода продукта на рынок - сложная задача для оптимизированных адаптируемых компиляторов. Такие компиляторы предоставляют надежные средства получения обратной связи, что помогает специализировать процессоры для прикладных областей применения. При выборе адаптируемого компилятора наиболее оптимальным может быть компилятор с открытым исходным кодом. В статье сравниваются два известных компилятора с открытым исходным кодом: GCC и LLVM. Первый – проверенный компилятор, адаптированный более чем под 100 процессоров, а второй – новая многообещающая разработка, поддерживающая менее 10 процессоров. Большим плюсом последней является релиз Red Hat (дистрибутив Linux), в котором вместо GCC используется LLVM. Статья содержит сравнение двух компиляторов с точки зрения простоты адаптации и поддержки специфичных для различных реализаций языков оптимизаций.


Введение

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

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

Адаптируемые компиляторы, в отличие от традиционных, являются модульными и имеют не только зависимые (в основном, в бэкенде), но и независимые от целевой архитектуры модули.

Цель данной статьи — проанализировать два компилятора с открытым исходным кодом: GCC, уже устоявшийся адаптируемый компилятор, и LLVM, новый адаптируемый компилятор, построенный на других принципах. Компилятор GNU (GCC) — один из самых широко используемых компиляторов C/C++ в мире. Он является основным инструментом для построения всех Embedded Linux и Android-систем, а также всех настольных и серверных операционных систем Linux и их приложений. Компилятор GNU также используется для построения множества коммерческих операционных систем реального времени (например, систем от ENEA, QNX и Wind River).

Глава «Представление программных инструкций» содержит обзор представления программных инструкций GCC и LLVM, выделяет их преимущества и недостатки. Глава «Генерация кода» содержит описание структуры компиляторов и анализ этапа генерации кода. Глава «Специфичные для целевой архитектуры оптимизации» представляет два подхода к специализированным оптимизациям (например, распределение регистров) и взаимодействиям между описанием целевой машины и оптимизационными запросами. Эта глава также подчеркивает возможность добавления специализированных оптимизаций и оценивает сложность этой задачи. Заключение содержит окончательный анализ и выводы.

Каждая глава описывает сначала подход GCC, а затем LLVM (исходя из старшинства).


Представление программных инструкций

Механизм описания машины в GCC успешно показал себя на примере большого количества целевых архитектур, под которые он был адаптирован. Этот механизм использует модель компиляции, которая подстраивается под конкретную архитектуру, используя ее описание и инстанцирование машинно-зависимых частей генерируемого компилятора.

Первый шаг процесса адаптации — понимание архитектуры целевого микропроцессора. Для этого необходимо обладать информацией о таких ключевых элементах, как файл регистра (регистры общего назначения и регистры специального назначения, если таковые имеются), модель конвейера процессора (существуют ли конфликты конвейера, помехи или передающая сеть) и набор инструкций (является ли он ортогональным). Второй шаг — определение двоичного интерфейса приложений (англ. Application Binary Interface, ABI). ABI содержит информацию о размере, размещении и структуре типов данных, определяет соглашения о вызове (способ передачи аргументов в функции, структура стекового кадра и соглашения об использовании регистров). Третий и последний шаг — определение трех ключевых файлов, которые будут описывать микропроцессор и его среду выполнения для GCC: программный макрофайл целевой машины (machine.h), файл описания машины (machine.md) и третий файл, содержащий вспомогательные функции для двух предыдущих файлов (machine.c). На первый взгляд, описания машин сложно читать, составлять, поддерживать и улучшать [11]. Они требуют уточнения паттернов инструкций при помощи шаблонов языка межрегистровых передач (англ. Register Transfer Language, RTL), применяя многословный, повторяющийся и требующий множества деталей механизм. На рисунке 1 показан типичный шаблон RTL описания машины MIPS (addsi3).


На рисунке 1 показан типичный шаблон RTL описания машины MIPS (addsi3).
Рисунок 1. GCC: пример описания инструкции.


Шаблон RTL использует операторы RTL set (отвечает за присваивание) и plus. Оператор match_operand сопоставляет операнд с шаблоном, используя режим (SI — четырехбайтовое целое), предикат (register_operand) и строки-ограничения (“=r”, ”r” и “r”). Получив GIMPLE выражение a = b + c, сначала генерируется инструкция RTL, а затем постепенно формируется инструкция по сборке. На многих устройствах не все нумерованные регистры эквивалентны. Так, одни регистры не могут использоваться при адресации с индексированием, а другие не могут быть использованы в некоторых инструкциях. Такие ограничения машины описываются компилятору при помощи классов регистров [10].

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

Обычно каждый регистр принадлежит нескольким классам. Кроме того, должны существовать класс ALL_REGS, содержащий все регистры, и класс NO_REGS, не содержащий регистров. Часто, объединением двух классов будет являться третий класс, однако, это не обязательно [10].

Один из классов должен называться GENERAL_REGS. Название этого класса — условное обозначение, сам класс задается ограничениями ‘r’ и ‘g’. Если между GENERAL_REGS и ALL_REGS нет отличий, GENERAL_REGS может быть определен как макрос, расширяющийся до ALL_REGS.

Отличные от GENERAL_REGS классы задаются ограничениями операндов с помощью машинно-зависимых ограничительных символов. Эти символы можно задать таким образом, чтобы они соответствовали нескольким классам, а затем использовать их в ограничениях операндов.

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

Если инструкция (или набор инструкций) принимает регистры от двух классов, то создается новый суммирующий класс. Например, если инструкция позволяет принимать в качестве операнда как регистр с плавающей точкой (регистр сопроцессора), так и регистр общего назначения, то следует создать класс FLOAT_OR_GENERAL_REGS, включающий оба регистра. В противном случае вы получите неоптимальный код или даже внутреннюю ошибку компилятора, при которой перезагрузка не сможет найти сформированный при помощи reg_class_subunion регистр в классе.

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

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

LLVM описывает три класса инструкций: L-инструкции, отвечающие за операции с памятью, А-инструкции для арифметических операций и J-инструкции, использующиеся при изменении потока выполнения (т. н. «прыжки»).

Описание целевой архитектуры выполняется на декларативном доменно-специфичном языке (наборе файлов .td), обработанном инструментом tblgen. Каждый файл описания дополняется файлом .cpp. Например, RegisterInfo.td имеет пару: файл RegisterInfo.cpp, который описывает файл регистра целевой архитектуры и все взаимодействия между регистрами. Физические регистры (которые на самом деле существуют в описании целевой архитектуры) — уникальные маленькие числа, а виртуальные регистры обычно представлены большими числами. Заметим, что регистр #0 зарезервирован для использования в качестве флагового значения. Каждый регистр в описании процессора сопоставлен с записью в TargetRegisterDesc, которая предоставляет текстовое имя регистра, используемое для вывода результатов сборки и файлов дампа отладки, и набор псевдонимов, используемый в качестве индикатора наложений регистров.

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


Рисунок 2. LLVM: описание машины через компилятор.

Рисунок 2. LLVM: описание машины через компилятор.


Класс TargetInstrInfo используется для описания машинных команд, поддерживаемых целевой архитектурой. Фактически, это массив объектов TargetInstrDescriptor, каждый из которых описывает одну команду. Дескрипторы определяют такие характеристики, как мнемокод операции, количество операндов, набор неявно определённых операторов регистра use и def, наличие у инструкции определенных независимых от целевой архитектуры качеств (доступ к памяти, заменяемость и т. д.) и машинно-зависимых флагов.

Генерация кода
GCC использует модифицированную версию модели компиляции Дэвидсона-Фрейзера [1]. Она отличается от традиционной модели Ахо-Ульмана [2], в которой выбор инструкций осуществляется из оптимизированного машинно-независимого промежуточного представления (англ. Intermediate Representation, IR). Чтобы обеспечить высокое качество генерируемого кода в модели Ахо-Ульмана, выбор инструкций из дерева осуществляется мозаичным методом на базе стоимости. При этом в промежуточном представлении преследуется цель покрыть тематическое дерево за счет инструкций, минимизирующих стоимость при помощи набора деревьев преобразований.

Модель Дэвидсона-Фрейзера использует простой выбор инструкций и оптимизирует уже выбранные инструкции. Расширитель генерирует примитивный машинно-зависимый код при помощи набора деревьев преобразований (чаще всего, это RTL-деревья) и реализации более простого мозаичного метода на базе структуры. Конечный код формируется при помощи распознавателя, который определяет инструкции (Inst), соответствующие межрегистровым передачам, которые представляют промежуточный код. Для адаптации компилятора в модели Дэвидсона-Фрейзера требуется переписать расширитель и распознаватель, содержащие простые алгоритмы [4]. Стандартный оптимизатор машинно-зависимого кода существует благодаря следующей ключевой идее: когда вычисления выражены в форме допустимых межрегистровых передач, несмотря на то, что сами инструкции межрегистровых передач машинно-зависимы, их форма не зависима. [5].

Одна из оптимизаций, выполняемых в процессе генерации кода, сравнение слиянием, показана на рисунке 3.


Рисунок 3. GCC: оптимизация в процессе генерации кода.

Рисунок 3. GCC: оптимизация в процессе генерации кода.


В LLVM генерация кода или выбор инструкций — это процесс преобразования кода LLVM, который был передан генератору кода, в машинно-зависимый код. В литературе описано несколько хорошо известных способов, как это можно сделать. LLVM использует селектор инструкций, основанный на SelectionDAG.

В настоящее время части селектора инструкций DAG генерируются из файлов описания целевой архитектуры (*.td). Цель LLVM заключается в том, чтобы селектор инструкций генерировался из этих файлов полностью. Однако на данный момент некоторые участки все еще требуют написания кода на С++.

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

SelectionDAG — направленный ациклический граф (англ. Directed Acyclic Graph, DAG), чьи узлы — экземпляры класса SDNode. Основная польза SDNode заключается в коде, который указывает тип операции и ее операнды. Различные типы узлов операций описаны в начале файла include/llvm/CodeGen/SelectionDAGNodes.h.


Узел SelectionDAG содержит следующую информацию:
  • код операции: целое число, определяющее инструкцию, представленную в узле;
  • результаты (определения): большинство инструкций производят один результат, но некоторые могут иметь несколько выходных значений (например, операции с побочными эффектами или комбинированные инструкции деления и деления с остатком) или не иметь их вовсе (например, команды перехода); объект узла содержит список типов значений для всех своих результатов;
  • операнды: каждый SDNode содержит список всех узлов, от которых он зависит.

SelectionDAG имеет входной и корневой узел. Входной узел всегда выступает в качестве узла-индикатора с кодом операции ISD::EntryToken. Корневой узел — последний побочный в цепочке токенов. Например, в простой функции базового блока корневым узлом будет являться узел return.

Важным принципом SelectionDAG является понятие допустимых и недопустимых ациклических графов. Допустимый DAG использует только поддерживаемые операции и типы. Например, на 32-битном PowerPC DAG со значением типа i1, i8, i16 или i64 будет недопустимым, как и DAG, использующий операции SREM или UREM. Фазы легализации типов и операций отвечают за превращение недопустимого DAG в допустимый.

Класс бэкенда targetTargetLowering, наследованный от TargetLowering, предлагает двойственный функционал. С одной стороны, он предоставляет своему суперклассу специфичную для целевой архитектуры информацию, включая:

  • расположение машинных функций (2 байта);
  • типы значений, изначально поддерживаемых целевой машиной, для каждого типа соответствующий класс регистра;
  • точное двоичное представление булевых значений (true: 1, false: 0).

С другой стороны, targetTargetLowering обрабатывает все случаи, в которых узлы-инструкции не могут быть приведены к машинному виду автоматически и требуют ручного вмешательства. В такие случаи входят соглашения о вызовах, специальные инструкции и операнды, которые нельзя автоматически понизить из-за определенных особенностей и ограничений архитектуры. Одним из недостатков LLVM в этом плане является то, что в CISC-архитектуре генерация кода/понижающая часть написаны вручную практически на 80%.

Важно, чтобы генератор кода знал о наличии фиксированных регистров. В частности, в потоке команд часто возникают участки, в которых распределитель регистров должен располагать определенное значение в определенном регистре. Причиной этому могут быть ограничения набора инструкций (например, x86 может выполнять только 32-битное деление при помощи EAX/EDX регистров) или внешние факторы, например, соглашения о вызове. В любом случае, селектор инструкций должен создавать код, который способен при необходимости скопировать виртуальный регистр в физический и наоборот.

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

MachineInstr изначально выбираются в SSA-представлении и сохраняют его, пока не произойдет распределение регистров. Это тривиальная задача, так как LLVM уже находится в SSA-форме. Узлы LLVM PHI становятся узлами PHI машинного кода, а виртуальным регистрам разрешено иметь только одно определение.

После распределения регистров машинный код больше не находится в SSA-представлении, так как в коде не остается виртуальных регистров.

Выбор инструкций, возможно, самая важная часть этапа генерации кода. Его задача заключается в конвертации допустимого DAG в DAG целевого машинного кода. Другими словами, абстрактные машинно-независимые данные на входе должны быть преобразованы в специализированные машинно-зависимые данные на выходе. Для этого LLVM использует сложный алгоритм сравнения паттернов, состоящий из двух основных шагов.

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

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

В заключение этой главы, LLVM использует хороший подход, но, чтобы добиться оптимизации кода, пользователь должен написать больше кода, чем при использовании GCC.


Специфичные для целевой архитектуры оптимизации

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

Локальная оптимизация сворачивает две или более последовательные инструкции в одну. Каждый паттерн описан в файле описания машины. Локальная оптимизация вызывается между распределением регистров и планированием команд [6]. Описание содержит входные и выходные команды, дополнительные рабочие регистры. Ниже приведен пример описания паттерна:


Рисунок 4. GCC: описание паттерна локальной оптимизации

Рисунок 4. GCC: описание паттерна локальной оптимизации.


Рисунок 5. GCC: локальное преобразование.

Рисунок 5. GCC: локальное преобразование.


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

Рисунок 6. GCC: разделение
Рисунок 6. GCC: разделение.


GCC предоставляет возможность выбора места размещения локальной/глобальной переменной:

Рисунок 7. GCC: размещение переменной в определенный регистр.

Рисунок 7. GCC: размещение переменной в определенный регистр.


Распределение регистров в GCC [10] основано на информации о регистрах, а точнее, о классах регистров и ограничениях инструкции. Чтобы избежать ошибок при распределении, в основу описания машины положены самые узкие классы регистров.

Существует несколько макросов, которые позволяют пользователю (тому, кто портирует новую архитектуру) конфигурировать и улучшать стандартное распределение. Некоторые макросы, даже если они используются для конфигурирования, требуют обширных знаний как реализации, так и архитектуры компилятора GCC. Одним из примеров может служить режим MODE_BASE_REG_REG_CLASS. Этот макрос применяется в случаях, когда архитектура имеет режимы индексной адресации, а база и индексированные адреса имеют требования, отличные от применяемых регистром требований к другой базе. Ловушка — другой механизм, который позволяет настраивать распределение регистров. Инструмент TARGET_CLASS_LIKELY_SPILLED_P (reg_class_t rclass) возвращает значение true, если псевдозначения, присвоенные регистрам класса rclass, близки к затиранию из-за того, что регистры класса rclass необходимы затертым регистрам.

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

Линейное сканирование применяется в LLVM в качестве распределителя регистров по умолчанию с 2004 года [6]. Несмотря на простоту, оно неожиданно хорошо работает. И эта простота облегчила внесение небольших улучшений в генерируемый код при помощи тонкой настройки алгоритма. Более продвинутые алгоритмы часто требуют построения объемных структур данных или делают допущения, касающиеся инвариантности времени жизни переменных. Это затрудняет коммутацию двухадресных команд «на лету» и рематериализацию загрузки пула констант вместо его вытеснения в стек.

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

LLVM предоставляет несколько различных алгоритмов распределения регистров.

Распределение регистров основано на описании регистров целевой архитектуры. Описание регистров в LLVM состоит из двух частей: описательная часть в файле TargetRegisterInfo.td и часть с кодом C++ [8].

Информация о регистрах генерируется в компиляторе при помощи инструмента TableGen из файла .td и помещается в файлы targetGenRegisterInfo.h.inc и targetGenRegisterInfo.inc. Однако часть кода в targetRegisterInfo требует ручной правки.

Стоит заметить, что автоматически сгенерированные файлы и классы не предоставляют полный регистровый функционал и информацию, необходимые для создания машинно-зависимого бэкенда (существуют некоторые виртуальные функции, определенные в классе TargetRegisterInfo, которые не реализуются в автоматически сгенерированном классе). Таким образом, необходимо вручную реализовывать недостающие функции, чтобы получить определенную информацию о регистрах (например, getCalleeSavedRegs() и getFrameRegister() и т. д.) [9].

Заключение
Адаптируемые компиляторы — многообещающий подход к решению проблем сокращения сроков вывода продуктов на рынок и ограниченной производительности. Компилятор является «адаптируемым», если его можно использовать для генерации кода для различных процессоров за счет повторного использования исходного кода компилятора. Появление такого подхода привело к сдвигу парадигмы в сторону методологии проектирования, опирающейся на язык описания архитектуры, совместную разработку компилятора/архитектуры и автоматическую генерацию компилятора/симулятора. Однако вне зависимости от используемого подхода точность и производительность зависят от специфичных для целевой архитектуры оптимизаций (выбор инструкций, распределение регистров и планирование команд).

На данный момент мы не можем назвать лучший адаптируемый компилятор. Каждый пользователь должен оценивать свои потребности. Какова целевая архитектура: маленький микроконтроллер, risc/cisc процессор или ядро, которое в будущем может стать частью многоядерной архитектуры? Другая составляющая принятия решения — баланс опыта работы с определенными компиляторами и потенциальные временные затраты на изучение новых.



Таблица 1. Сравнение GCC и LLVM.

Компиляторы

GCC LLVM
Описание машины RTL Декларативный язык описания
Генерация кода Дэвидсон-Фрейзер SelectionDAG
Локальный движок Локальные паттерны в файле .md -
Движок разделения Паттерны разделения в файле .md -
Распределение регистров 2 шага – локальный и глобальный Линейное сканирование


GCC сохраняет гибкость, позволяющую создавать нечто уникальное для каждой целевой архитектуры. GCC — очень зрелый и простой в установке компилятор (для большинства систем), является компилятором по умолчанию во множестве систем.

С моей точки зрения, плюс LLVM в легкости добавления новых возможностей, построения и отладки, в то время как по производительности GCC все еще превосходит LLVM на большинстве целевых архитектур. Однако стоит заметить, что LLVM была разработана для многоядерного и многопоточного программирования. Например, начиная с 2015 года, LLVM полностью поддерживает стандарт OpenMP, основываясь на библиотеке LLVM OpenMP. Эта особенность улучшает производительность приложения на современных многоядерных архитектурах.

Список литературы
  1. Norman Ramsey, Jack W. Davidson, “Machine descriptions to build tools for embedded systems”, In Workshop on Languages, Compilers, and Tools for Embedded Systems, Springer Verlag, volume 1474 of lncs, 1998 (June), pages 172–188.
  2. Alfred V. Aho, Mahadevan Ganapathi, Steven W. K. Tjiang, “Code generation using tree matching and dynamic programming”, Transactions on Programming Languages and Systems, 1989, 11(4), pages 491–516.
  3. A. V. Aho, R. Sethi, J. D. Ullman, “Compilers: Principles, Techniques, and Tools”, Addison-Wesley, 1986.
  4. Mark W., Bailey, Jack W. Davidson, “Automatic detection and diagnosis of faults in generated code for procedure calls”, IEEE Transactions on Software Engineering, 29(11), 2003, pages 1031–1042.
  5. Christopher W. Fraser, Robert R. Henry, Todd A. Proebsting, “BURG: fast optimal instruction selection and tree parsing”, SIGPLAN Notices, 1992, 27(4), pages 68–76.
  6. http://llvm.org/
  7. Dmitry Melnik, Andrey Belevantsev, Dmitry Plotnikov, Semun Lee, “A case study: optimizing GCC on ARM for performance of libevas rasterization library,” In Proceeding of GROW [2010].
  8. Ghassan Shobaki, Maxim Shawabkeh, Najm Abu-Rmaileh, “Preallocation Instruction Scheduling with Register Pressure Minimization Using a Combinatorial Optimization Approach”, ACM Transactions on Architecture and Code Optimization, TACO, Sep. 2013.
  9. http://www.pitt.edu/~yol26/notes/llvm3/LLVM3.html
  10. https://gcc.gnu.org/onlinedocs/gccint/Register-Classes.html
  11. http://www.drdobbs.com/retargeting-the-gnu-c-compiler/184401529
  12. https://opus4.kobv.de/opus4-fau/files/1108/tricore_llvm.pdf

Теги: оптимизированный адаптируемый компилятор, GCC, LLVM, адаптируемая генерация кода, адаптируемые оптимизации