Тема непосредственно самого процессора Эльбрус уже достаточно хорошо раскрыта в различных статьях, в том числе, в статьях, опубликованных на habr.ru. Кроме самого процессора Эльбрус время от времени на просторах рунета можно встретить обсуждения, посвященные вопросу портирования компилятора clang/llvm на Эльбрус. Пожалуй, данная тема еще не получала подробного освещения. Данная заметка призвана заполнить этот «пробел».
Если вкратце, то llvm из e2k-дистрибутива портируется на базе оптимизирующего компилятора LCC. Ну а тем, кому нужно больше подробностей, то добро пожаловать «под кат».
Компилятор LCC
Компилятор LCC – это оптимизирующий компилятор и система компиляторной поддержки, включая специализированные версии библиотек, для процессоров семейства Эльбрус, который разрабатывается в МЦСТ примерно с 1997. Есть также родственный бинарный оптимизирующий компилятор для двоичной трансляции x86-кодов. Если углубиться в историю еще дальше, то история МЦСТ и процессоров Эльбрус восходит в общем и целом еще к научной школе академика Лебедева — главного конструктора машины БЭСМ-6. И носит непрерывный характер в части передачи опыта между разработчиками процессорных архитектур и компиляторов в рамках смены поколений на протяжении уже около полувека. Даже «лихие» 90-е не прервали эту линию развития, в том числе и благодаря немалым усилиям коллектива по настоящему увлеченных своим делом сотрудников.
Исходно входными языками программирования для компилятора LCC являлись такие языки как С/C++ и Fortran, и первоначальное название компилятора — ecf_opt, что является сокращением от Elbrus C(C++), Fortran OPTimizer. В качестве собственно фронтенда языков C/C++ использовался и используется сторонний EDG-фронтенд. Главные задачи, решаемые командой из МЦСТ по разработке оптимизирующего компилятора LCC, состоят в разработке архитектурно-независимых оптимизаций и в разработке специализированных архитектурно-зависимых оптимизаций для процессора Эльбрус. В состав архитектурно-зависимых задач также естественно входят такие задачи как финальное планирование и распределение регистров. На сегодняшний день одной из основных ценностей проекта LCC в контексте данного разговора является прежде всего набор архитектурно-зависимых оптимизаций для процессора Эльбрус и связанных с этим проектом компетенциях.
Если посмотреть повнимательней на рабочее e2k-окружение, то легко заметить, что непосредственно программа lcc – это драйвер, а собственно программа оптимизирующего компилятора — это ecf_opt. Поэтому будем говорить, что рассматриваемая в данной заметке версия llvm-e2k портируется с помощью оптимизирующего e2k-бэкенда на базе компилятора ecf_opt (из состава LCC).
E2K-бэкенды
Из известных мне e2k-бэкендов, которые применяются на практике на сегодняшний день, перечислю, пожалуй, следующие — оптимизирующий компилятор ecf_opt и компилятор есс уровня-O0, оптимизирующий бинарный компилятор rtc_opt для исполнения x86-кодов и, наконец, оптимизирующий бэкенд tangoLib для виртуальных машин с jit-компиляцией.
E2K-БЭКЕНД | ОПТИМИЗАЦИЯ | ПРИМЕНЕНИЕ |
ecf_opt | оптимизирующий | C/C++, Fortran |
ecc | “уровня-O0” | C/C++, Fortran |
rtc_opt | оптимизирующий | x86-коды |
tangoLib | оптимизирующий | Java/JS/.NET |
Все эти проекты созданы в рамках ОКРов, поэтому их кодовая база не может быть использована для opensource-проектов без согласования с заказчиком, профинансировавшим разработку. В целом разработка e2k-бэкендов возможна, задача решаема, но это не самая простая задача. А самое главное разработка оптимизирующего (!) e2k-бэкенда едва ли может быть выполнена «одиночкой», требует команды квалифицированных разработчиков и в целом достаточно продолжительного периода разработки. Как показывает практика недооценка сложности разработки архитектурно-зависимой (!) части (очень значительно выходящей за рамки распределителя регистров и планировщика) оптимизирующего бэкенда для нетривиального VLIW'а по сравнению с такими процессорами как x86/arm/mips является вполне естественной. И в целом часто недооценка сложностей — это хорошо и полезно для общего развития и прогресса. Но единственное, что хотелось бы подчеркнуть особо. Тем, кто решил делать e2k-бэкенд, необходимо готовиться к длительной и напряженной многолетней работе. Оценка объема работ на основе опыта разработки кодогенераторов для x86/arm/mips здесь только введет в заблуждение и может сыграть злую шутку. И не надо говорить в таком случае потом, что относительно сложности задачи разработки e2k-бэкенда их не предупреждали, и что мол патроны (процессор) не той системы.
Заметим, что у компании Intel есть проприетарный x86-бэкенд в составе компилятора icc. При этом мы все конечно привыкли к достижениям opensource-движения, связанного с именем Ричарда Столлмана. Формат opensource-движения подарил много замечательных проектов (созданных правда при этом на средства крупных IT-компаний, о чем как правило забывают). Это и linux, и gcc, и clang/llvm и много и много еще чего. С другой стороны, как часто при разработке ПО общего назначения приходится заглядывать в файл с кодом распределения регистров для x86 или уточнять в коде планировщика правила планирования конкретной инструкции для arm? Каковы принципиальные отличия для разработчиков ПО на языке C++ в плане архитектурной оптимизации кода скажем под x86 от того, что будет использован компилятор icc с проприетарным x86-бэкендом или компиляторы gcc/clang c opensource-бэкендами? При том, что компилятор icc давно уже базируется на проекте clang/llvm. Да, для процессора Эльбрус с opensource-бэкендами на сегодняшний день не «густо», на все свои причины, в том числе ресурсного характера. Тем не менее, данное обстоятельство не является препятствием на пути разработки/портирования ПО общего назначения для Эльбруса, как не является препятствием факт использования готовых бинарных сборок компиляторов clang/gcc в рамках x86-дистрибутива.
Внешний бэкенд
Давайте теперь рассмотрим вопрос — а надо ли при портировании llvm на Эльбрус обязательно делать бэкенд в рамках кодовой базы llvm. Безо всяких вопросов в компиляторе libLLVM есть огромное количество замечательных и полезных архитектурно-независимых оптимизаций, анализов и различных отладочных и других вспомогательных инструментов. А самое главное - много очень важных фронтендов. Что же касается ситуации с разработкой архитектурно-зависимой (!) части оптимизирующего бэкенда для VLIW-процессоров общего назначения с нетривиальной системой команд, то все уже не столь радужно. Так, Hexagon — это все-таки в большей степени dsp-процессор, а не general-purpose процессор. Был конечно экспериментальный бэкенд для Itanium'а. Но он именно, что «был», и был при этом не самым любимым «дитём» в мире opensource'а. Чего только стоят высказывания Линуса Торвальдса про Itanium. Не достигал и высокого уровня производительности бэкенд для Itanium'а в сравнении с проприетарным бэкендом от Intel. И что самое главное, хоть Itanium и Эльбрус и относятся к так называемому типу VLIW-процессоров общего назначения, но между ними есть существенные различия на аппаратном уровне. Наверное в этом смысле все VLIW-процессоры из разных линеек — это совершенно разные и мало-похожие друг на друга процессоры на уровне системы команд. А различия в аппаратных механизмах во VLIW-процессоре — это совершенно различные архитектурно-зависимые оптимизации с точки зрения оптимизирующего бэкенда. Собственно необходимость разработки в libLLVM совершенно новой всего лишь только инфраструктуры для создания эффективного VLIW-бэкенда для Эльбруса, и это еще пока без учета разработки самих новых архитектурно-зависимых оптимизаций с нуля, в частности, отмечают и сами опытные llvm-разработчики в рамках встречи «LLVM Meetup при поддержке C++Russia» (https://www.youtube.com/watch?v=r_P0wm7d_0k).
С этой точки зрения, решение об использовании для портирования llvm на процессор Эльбрус уже разработанного e2k-бэкенда из компилятора LCC, на разработку и отладку (!) которого было затрачено значительное количество человеко-часов, да еще с учетом сравнительно скромных ресурсных возможностей МЦСТ (в количественном смысле), выглядит в целом как достаточно оптимальное решение.
Почему были сделаны такие вводные достаточно длинные замечания. Дело в том, что разработчики, которые уже значительное время занимались и занимаются разработкой непосредственного самого llvm, м-м-м … Как бы это сказать? Что-ли «в штыки» воспринимают саму идею внешнего по отношению к кодовой базе llvm (с организационной точки зрения) бэкенда. И не важно является ли это бэкенд проприетарным или opensource-бэкендом. К сожалению, при этом редко приводятся по-настоящему рациональные доводы. Делать внешний бэкенд не в рамках проекта llvm-project «неправильно» просто потому, что это «неправильно». При этом совершенно игнорируются указанные инженерно-технические свойства и ресурсно-административные ограничения в контексте процессора Эльбрус.
Промежуточные представления LLVM-IR и EIR
Собственно теперь начнем переходить к техническим деталям. В компиляторе libLLVM есть замечательное промежуточное представление llvm-IR. При его разработке был учтен значительный опыт предыдущей разработки компиляторов. В целом представление llvm-IR получилось очень удачным, что в том числе и способствовало его такому широкому распространению к сегодняшнему дню. Если присмотреться к устройству промежуточного представления llvm-IR, то за ним для взгляда «матерого» компиляторщика в значительной степени легко просматривается язык Си, особенно если убрать все эти «необязательные» надстройки в виде if/for/while (goto – наше все). А язык Си (но только ни в кое случае ни C++) — это, как уже давно было сказано, и есть изначально «высокоуровневый ассемблер».
На этапе llvm-IR в компиляторе libLLVM реализовано огромное количество архитектурно-независимых оптимизаций. И это снова очень удачное исходное проектное решение. После завершения архитектурно-независимого этапа наступает очередь архитектурно-зависимого этапа, для применения оптимизаций, основанных на аппаратных свойствах конкретного процессора, вынесенных на уровень системы команд (то есть ассемблера). Для завершающего этапа эмиссии итогового ассемблера в виде текста (или структур данных в памяти) в libLLVM есть универсальная print/write-инфраструктура. По мимо этого есть определенные базовые реализации C++-классов для планирования, распределения на физические регистры, система tablegen для генерации описания архитектуры. Но в целом каждый конкретный бэкенд под свой тип процессора может и скорее всего использует свои собственные реализации классов или части классов.
С другой стороны в компиляторе ecf_opt (LCC) изначально было и есть промежуточное представление EIR. Разработано представление EIR уже более 20+ лет назад, так что наверное можно говорить о том, что промежуточные представления EIR и llvm-IR “ровесники”. Изначально и представление llvm-IR, и представление EIR были ориентированы на выражении свойств программ на языке Си. Также видимо при разработке представлений EIR и llvm-IR были вполне вероятно прочитаны одни и те же книги и статьи. Поэтому наверное нет ничего удивительно в том, что даже по прошествии стольких лет, промежуточные представления EIR и llvm-IR обладают достаточно близким устройством. В целом с некоторого момента времени устройство llvm-IR все-таки стало развиваться быстрее устройства EIR. Но имеющиеся различия на сегодняшний день по-прежнему не носят принципиального характера. Пожалуй, единственным очень неприятным отличием на момент начала работ по портированию llvm на базе ecf_opt, было отсутствие в представлении EIR аналога операции invoke для zerocost-реализации exception'ов. (Исходно в ecf_opt использовался механизм на базе setjmp/longjmp.) Но благодаря усилиям, в том числе, Сергея Баранникова и Романа Русяева (сотрудникам МЦСТ на тот момент), в компиляторе LCC была реализована поддержка invoke-операций и zerocost-механизма в целом. Это очень значительно упростило процесс портирования llvm на Эльбрус в части компиляции C++-программ.
Транслятор LLVM-IR в EIR
Таким образом, в силу изначальной схожести представлений llvm-IR и EIR при старте работ по портированию llvm в отделе языковых технологий (еще около 8 лет назад) было выбрано решение по разработке транслятора. Основная задача транслятора при этом состояла в преобразовании llvm-IR (сразу после завершения архитектурно-независимого этапа) в представление EIR. После чего ecf_opt в роли бэкенда занимается компиляцией представления EIR в e2k-ассемблер, выполняя при это все реализованные в нем за много лет разработки архитектурно-зависимые оптимизации и учитывая все необходимые особенности каждой из версий системы команд и процессора. И на завершающем этапе компиляции ecf_opt естественно занимается распределением регистров и планированием кода.
Как и для всякой инженерной задачи для задачи портирования llvm на Эльбрус могут имплементироваться различные решения. И поиск решений может проводится в разных направлениях. Но при этом решение по портированию llvm на Эльбрус на базе компилятора ecf_opt уже продемонстрировало принципиальную работоспособность и несомненную практическую пользу. Особенно с учетом требуемых на разработку и сопровождение ресурсов (в частности в компиляторе ecf_opt не потребовалось выполнять каких-то серьезных переделок).
В итоге при выбранном подходе компилятор ecf_opt по сути выполняет все те же самые функции, что и стандартные llvm-бэкенды архитектурно-зависимого этапа для x86, arm и других процессоров. Но если только последние являются на уровне исходных кодов отдельными модулями в составе проекта llvm, то ecf_opt-бэкенд на уровне исходных кодов существует как самостоятельный проект. (Утрируя — может летать, плавать и крякать как и все остальные утки.) При всем при этом точно также сохраняется возможность применения стандартных архитектурно-независимых llvm-оптимизаций и llvm-анализов при компиляции под Эльбрус. Например, результаты ipa-анализов из llvm-IR после работы транслятора напрямую «пробрасывались» и использовались в компиляторе ecf_opt.
Внешнее API для LCC
Теперь, что касается непосредственно ряда технических особенностей реализации процесса трансляции llvm-IR в EIR. Примерно за первый год разработки транслятора было опробовано несколько реализаций. По результатам первых опытов в итоге была выбрана реализация с использованием транзитного промежуточного представления. Сама по себе выбранная реализация с транзитным представлением не является строго обязательной. Однако обладает рядом тонких свойств, с не очевидными на первый взгляд следствиями. В полной мере рассмотрение всех деталей и причин выбора реализации с транзитным представлением потребовало бы много времени, поэтому если и говорить о них всех детально, то как-нибудь в другой раз.
Транзитное представление получило название lccrt-IR, а библиотека с реализацией — liblccrt. Тут дело еще в том, что представление EIR не имело внешнего API-интерфейса для создания компилируемого модуля в терминах EIR'а. И транзитное промежуточное представление lccrt-IR взяло на себе такую задачу внешнего-API. В значительной мере представление lccrt-IR может рассматриваться как lite-версия представления EIR. За той важной оговоркой, что представление EIR является частью проприетарной кодовой базы. А представление lccrt-IR изначально развивалось как opensource-проект в роли открытого внешнего API со встроенной поддержкой со стороны компилятора ecf_opt.
Можно провести такое сравнение. Если llvm-IR выступает в качестве внешнего API для компилятора LLVM, библиотека libgccjit выступает в качестве внешнего API для компилятора GCC. То совершенно аналогично представление lccrt-IR и библиотека liblccrt выступают в качестве внешнего API для компилятора LCC.
КОМПИЛЯТОРНЫЙ ПРОЕКТ | ПОЛЬЗОВАТЕЛЬСКАЯ БИБЛИОТЕКА | ПРОМЕЖУТОЧНОЕ ПРЕДСТАВЛЕНИЕ |
LLVM | libLLVM | llvm-IR |
GCC | libgccjit | GIMPLE |
LCC | liblccopt | lccrt-IR |
Транслятор GIMPLE в EIR
С момента своего появления представление lccrt-IR с принципиальной точки зрения позволяет разрабатывать сторонние фронтенды для компилятора ecf_opt. Сейчас, например, внутри МЦСТ рассматривается вариант с портированием компилятора gcc на базе оптимизирующего компилятора ecf_opt. О чем здесь идет речь. В компиляторе GCC есть представление GIMPLE, правда в отличии от llvm-IR без внешнего стандарта. Есть также в gcc архитектурно-независимые оптимизации на представлении GIMPLE и точка преобразования GIMPLE в представление архитектурно-зависимого этапа RTL. На таком уровне рассмотрения схема работы компилятора GCC совпадает со схемой работы компилятора LLVM. В первом приближении представление GIMPLE выглядит подходящим для разработки транслятора, который позволит транслировать GIMPLE в EIR. И точно также в первом приближении задача портирования компилятора gcc на Эльбрус может решаться через разработку транслятора GIMPLE в lccrt-IR, как это уже сделано для LLVM.
Что могло бы быть получено при таком способе портирования — это архитектурно-независимые оптимизации компилятора gcc, и самое главное и важное — это gcc-фронтенд языков C/C++ и других. В частности, могла бы быть получена версия gccgo на базе оптимизирующего компилятора ecf_opt. (Текущая версия gccgo-e2k реализована на базе «неоптимизирующего» e2k-бэкенда.) Что не будет получено — архитектурно-зависимая часть все также будет проприетарной. На последнее обстоятельство стоит обратить пристальное внимание, чтобы было четкое понимание, что в данном случае стоит за фразой «портирование gcc на базе ecf_opt», и не строить ошибочных ожиданий.
Теоретически портирование gcc на Эльбрус через разработку транслятора GIMPLE в lccrt-IR могла бы вестись командой заинтересованных разработчиков сразу в рамках opensource-проекта. К сожалению, в рамках МЦСТ на текущий момент недостаточно разработчиков для решения задачи портирования gcc на базе оптимизирующего компилятора ecf_opt.
Пример с (возможным) построением транслятора GIMPLE в lccrt-IR является так раз наглядной демонстрацией одного из следствий выбора реализации с транзитным промежуточным представлением lccrt-IR.
OPENSOURCE-проекты
Ознакомиться с проектом liblccrt можно на ресурсе: https://gitflic.ru/project/e2khome/lccrt. Собственно там же можно ознакомиться и с транслятором из llvm-IR в lccrt-IR: https://gitflic.ru/project/e2khome/llvm-project-e-2-k.
Особо подчеркну, что из проектов на gitflic.ru на текущий момент нет возможности собрать компилятор clang-e2k/llvm-e2k, который будет выдавать e2k-ассемблер. Чтобы получить полноценную сборку компилятора clang-e2k/llvm-e2k нужно еще дополнительно иметь на руках бинарную сборку компилятора ecf_opt (в виде lccopt-компоненты). При этом версия бинарной сборки должна быть согласована с версий llvm-e2k. Замечу, что вопрос регулярной публикации бинарных сборок (cross и native) для компилятора ecf_opt, сейчас рассматривается. Надеюсь он получит положительное решение. Как запасной вариант для получения завершенной сборки, прежде всего для тех у кого уже есть доступ к e2k-дистрибутиву, параллельно прорабатывается вариант с lcbe-бэкендом (не путать с llvm-bce). Задача lcbe-бэкенда состоит в преобразовании lccrt-IR пусть и с потерями производительности в компилируемый и исполняемый Си-код.
И еще особенно подчеркну, что проект llvm-e2k на gitflic.ru в данном контексте стоит рассматривать сейчас скорее в ознакомительных целях, в том смысле, что в рамках данного ресурса не ведется публикация релизов. Проект llvm-e2k на сайте gitflic.ru — это сугубо «unstable trunk», который может использоваться исключительно только на свой страх и риск. Этот момент конечно будет в дальнейшем исправляться, но сейчас данное обстоятельство необходимо строго обязательно иметь в виду.
Заключение
Была рассмотрена схема принципиального устройства порта llvm-e2k, а также рассмотрен ряд обстоятельств, повлиявших на выбор способа портирования. Если тематика портирования clang/llvm на базе компилятора LCC/ecf_opt окажется интересной для разработчиков, взаимодействующих с процессорами Эльбрус, и не только для них, то можно будет дополнительно и более подробно рассмотреть те или иные вопросы, связанные с данным портом llvm.
Что же касается тематики портирования gcc на базе компилятора LCC/ecf_opt, если идея данного проекта в свою очередь окажется востребованной, то можно будет создать отдельную площадку для обсуждения связанных вопросов и возможных действий по портированию.