Какво е LLVM? Силата зад Swift, Rust, Clang и др

Новите езици и подобренията на съществуващите се разрастват в целия пейзаж на разработката. Mozilla's Rust, Apple Swift, Jetbrains's Kotlin и много други езици предоставят на разработчиците нов избор от възможности за скорост, безопасност, удобство, преносимост и мощност.

Защо сега? Една голяма причина са новите инструменти за изграждане на езици - по-специално компилаторите. И главен сред тях е LLVM, проект с отворен код, първоначално разработен от създателя на езика Swift Chris Lattner като изследователски проект в Университета на Илинойс.

LLVM улеснява не само създаването на нови езици, но и подобряването на развитието на съществуващите. Той осигурява инструменти за автоматизиране на много от най-неблагодарните части на задачата за създаване на език: създаване на компилатор, пренасяне на изведения код на множество платформи и архитектури, генериране на специфични за архитектурата оптимизации като векторизация и писане на код за обработка на метафори на общоезиков език като изключения. Неговото либерално лицензиране означава, че може свободно да се използва повторно като софтуерен компонент или да се използва като услуга.

Списъкът на езиците, използващи LLVM, има много познати имена. Езикът Swift на Apple използва LLVM като своя компилаторна рамка, а Rust използва LLVM като основен компонент на своята верига от инструменти. Също така, много компилатори имат LLVM издание, като Clang, компилаторът C / C ++ (това е името, „C-lang“), самият проект е тясно свързан с LLVM. Mono, изпълнението на .NET, има опция за компилиране в собствен код с помощта на LLVM back end. И Kotlin, номинално JVM език, разработва версия на езика, наречен Kotlin Native, който използва LLVM за компилиране в машинен код.

Дефиниран LLVM

В основата си LLVM е библиотека за програмно създаване на машинен код. Разработчик използва API, за да генерира инструкции във формат, наречен междинно представяне или IR. След това LLVM може да компилира IR в самостоятелен двоичен файл или да извърши JIT (точно навреме) компилация върху кода, който да се изпълнява в контекста на друга програма, като интерпретатор или време за изпълнение на езика.

Приложните програмни интерфейси на LLVM предоставят примитиви за разработване на много често срещани структури и модели, открити в езиците за програмиране. Например, почти всеки език има концепцията за функция и за глобална променлива, а много от тях имат съпрограми и интерфейси с чужда функция. LLVM има функции и глобални променливи като стандартни елементи в своя IR и има метафори за създаване на съпрограми и взаимодействие с C библиотеки.

Вместо да отделяте време и енергия за преоткриване на тези конкретни колела, можете просто да използвате реализациите на LLVM и да се съсредоточите върху частите от вашия език, които се нуждаят от вниманието.

Прочетете повече за Go, Kotlin, Python и Rust 

Отивам:

  • Докоснете силата на езика Go на Google
  • Най-добрите IDE и редактори на език Go

Котлин:

  • Какво е Kotlin? Обяснена е алтернативата на Java
  • Рамки на Kotlin: Проучване на инструментите за разработка на JVM

Python:

  • Какво е Python? Всичко, което трябва да знаете
  • Урок: Как да започнем с Python
  • 6 основни библиотеки за всеки разработчик на Python

Ръжда:

  • Какво е Ръжда? Начинът за безопасно, бързо и лесно разработване на софтуер
  • Научете как да започнете с Rust 

LLVM: Проектиран за преносимост

За да се разбере LLVM, може да е полезно да се разгледа аналогия с езика за програмиране C: C понякога се описва като преносим асемблерен език на високо ниво, тъй като има конструкции, които могат да се свържат в близост със системния хардуер и е пренесен на почти всяка архитектура на системата. Но C е полезен като преносим асемблерен език само до точка; не е проектиран за тази конкретна цел.

За разлика от тях, IR на LLVM е проектиран от самото начало да бъде преносим монтаж. Един от начините да постигне тази преносимост е като предлага примитиви, независими от която и да е конкретна архитектура на машината. Например целочислените типове не се ограничават до максималната битова ширина на основния хардуер (като 32 или 64 бита). Можете да създадете примитивни цели числа, като използвате толкова бита, колкото е необходимо, като 128-битово цяло число. Също така не е нужно да се притеснявате за създаването на изход, който да съответства на набор от инструкции на конкретен процесор; LLVM се грижи за това и за вас.

Архитектурно неутралният дизайн на LLVM улеснява поддържането на хардуер от всякакъв вид, настоящ и бъдещ. Например, наскоро IBM допринесе с код за поддръжка на z / OS, Linux on Power (включително поддръжка на библиотеката за векторизация на MASS на IBM) и AIX архитектури за LLVM C, C ++ и Fortran проекти. 

Ако искате да видите примери на живо за LLVM IR, отидете на уебсайта на проекта ELLCC и изпробвайте демонстрацията на живо, която преобразува C кода в LLVM IR направо в браузъра.

Как езиците за програмиране използват LLVM

Най-често срещаният случай на използване на LLVM е като компилатор за предшестващо време (AOT) за даден език. Например проектът Clang, изпреварващ времето, компилира C и C ++ в собствени двоични файлове. Но LLVM прави възможни и други неща.

Точно навреме компилиране с LLVM

Някои ситуации изискват код да се генерира в движение по време на изпълнение, вместо да се компилира предварително. Езикът Julia, например, JIT компилира своя код, защото трябва да работи бързо и да взаимодейства с потребителя чрез REPL (read-eval-print loop) или интерактивен ред. 

Numba, пакет за математическо ускорение за Python, JIT компилира избрани функции на Python в машинен код. Той може също така да компилира декориран от Numba код преди време, но (като Джулия) Python предлага бързо развитие, като е интерпретиран език. Използването на JIT компилация за създаване на такъв код допълва интерактивния работен процес на Python по-добре от компилацията преди време.

Други експериментират с нови начини за използване на LLVM като JIT, като компилиране на заявки за PostgreSQL, което води до петкратно увеличение на производителността.

Автоматична оптимизация на кода с LLVM

LLVM не само компилира IR в родния машинен код. Можете също така програмно да го насочите, за да оптимизирате кода с висока степен на детайлност, през целия процес на свързване. Оптимизациите могат да бъдат доста агресивни, включително неща като вграждане на функции, елиминиране на мъртъв код (включително неизползвани декларации за тип и аргументи на функции) и разгъване на цикли.

Отново силата е в това да не се налага да прилагате всичко това сами. LLVM може да се справи с тях вместо вас или можете да го насочите, за да ги изключите, ако е необходимо. Например, ако искате по-малки двоични файлове на цената на някаква производителност, можете да накарате вашия компилатор отпред да каже на LLVM да деактивира разгъването на цикъла.

Езици, специфични за домейн с LLVM

LLVM е използван за създаване на компилатори за много езици с общо предназначение, но също така е полезен за създаване на езици, които са силно вертикални или са изключителни за проблемен домейн. В някои отношения това е мястото, където LLVM блести най-силно, защото премахва голяма част от трудностите при създаването на такъв език и го кара да се представя добре.

Проектът Emscripten, например, взема LLVM IR код и го преобразува в JavaScript, на теория позволявайки на всеки език с LLVM заден край да експортира код, който може да работи в браузъра. Дългосрочният план е да има LLVM базирани задни краища, които могат да създават WebAssembly, но Emscripten е добър пример за това колко гъвкав може да бъде LLVM.

Друг начин, по който LLVM може да се използва, е да се добавят специфични за домейн разширения към съществуващ език. Nvidia използва LLVM, за да създаде компилатора на Nvidia CUDA, който позволява на езиците да добавят естествена поддръжка за CUDA, която се компилира като част от родния код, който генерирате (по-бързо), вместо да бъде извикан чрез библиотека, доставена с него (по-бавно).

Успехът на LLVM с специфични за домейн езици подтикна нови проекти в LLVM за справяне с проблемите, които те създават. Най-големият проблем е как някои DSL се превеждат трудно в LLVM IR, без много усилена работа на предния край. Едно от решенията в работата е многостепенното междинно представителство или проектът MLIR.

MLIR предоставя удобни начини за представяне на сложни структури от данни и операции, които след това могат да бъдат автоматично преведени в LLVM IR. Например, рамката за машинно обучение TensorFlow може да има много от сложните операции на графики на потока от данни, ефективно компилирани в собствен код с MLIR.

Работа с LLVM на различни езици

Типичният начин за работа с LLVM е чрез код на език, който ви е удобен (и който има поддръжка за библиотеките на LLVM, разбира се).

Два често срещани езика са C и C ++. Много разработчици на LLVM по подразбиране използват един от тези два по няколко добри причини: 

  • Самият LLVM е написан на C ++.
  • Приложните програмни интерфейси на LLVM се предлагат във въплъщения на C и C ++.
  • Много езиково развитие обикновено се случва с C / C ++ като основа

И все пак тези два езика не са единственият избор. Много езици могат да се обаждат в C библиотеки, така че теоретично е възможно да се извърши разработка на LLVM с всеки такъв език. Но помага да има действителна библиотека на езика, която елегантно обгръща API на LLVM. За щастие много езици и езикови изпълнения имат такива библиотеки, включително C # /. NET / Mono, Rust, Haskell, OCAML, Node.js, Go и Python.

Едно предупреждение е, че някои от езиковите обвързвания към LLVM може да са по-малко пълни от други. Например с Python има много възможности за избор, но всеки варира в своята пълнота и полезност:

  • llvmlite, разработен от екипа, който създава Numba, се очерта като настоящ претендент за работа с LLVM в Python. Той изпълнява само подмножество от функционалността на LLVM, както е продиктувано от нуждите на проекта Numba. Но това подмножество осигурява по-голямата част от това, което потребителите на LLVM имат нужда. (llvmlite обикновено е най-добрият избор за работа с LLVM в Python.)
  • Проектът LLVM поддържа собствен набор от обвързвания към C API на LLVM, но в момента те не се поддържат.
  • llvmpy, първото популярно свързване на Python за LLVM, изпадна в поддръжка през 2015 г. Лошо за всеки софтуерен проект, но по-лошо при работа с LLVM, предвид броя на промените, които се появяват във всяко издание на LLVM.
  • llvmcpy има за цел да актуализира връзките на Python за библиотеката C, да ги поддържа актуализирани по автоматизиран начин и да ги прави достъпни с помощта на родните идиоми на Python. llvmcpy все още е в ранните етапи, но вече може да свърши някаква елементарна работа с LLVM API.

Ако ви интересува как да използвате LLVM библиотеки за изграждане на език, собствените създатели на LLVM имат урок, използващ C ++ или OCAML, който ви стъпва през създаването на прост език, наречен Kaleidoscope. Оттогава е пренесен на други езици:

  • Haskell:  Директен порт на оригиналния урок.
  • Python: Единият такъв порт следи отблизо урока, докато другият е по-амбициозно пренаписване с интерактивен команден ред. И двамата използват llvmlite като обвързващи към LLVM.
  • Rust  and  Swift: Изглеждаше неизбежно да получим портове от урока за два от езиците, за които LLVM помогна да се създаде.

И накрая, урокът е достъпен и на  човешки езици. Преведена е на китайски, използвайки оригиналните C ++ и Python.

Какво не прави LLVM

С всичко, което LLVM предлага, е полезно да знаете и какво не прави.

Например LLVM не анализира граматиката на даден език. Много инструменти вече вършат тази работа, като lex / yacc, flex / bison, Lark и ANTLR. Анализирането така или иначе е предназначено да бъде отделено от компилацията, така че не е изненадващо, че LLVM не се опитва да се справи с нищо от това.

LLVM също не адресира пряко по-голямата култура на софтуер около даден език. Инсталиране на двоичните файлове на компилатора, управление на пакети в инсталация и надграждане на веригата от инструменти - трябва да направите това сами.

И накрая, и най-важното, все още има общи части от езици, за които LLVM не предоставя примитиви. Много езици имат някакъв начин за управление на паметта, събирана от боклука, или като основен начин за управление на паметта, или като допълнение към стратегии като RAII (които C ++ и Rust използват). LLVM не ви дава механизъм за събиране на боклук, но предоставя инструменти за внедряване на събирането на боклук, като позволява кодът да бъде маркиран с метаданни, което улеснява писането на събирачи на боклук.

Нищо от това обаче не изключва възможността LLVM в крайна сметка да добави собствени механизми за внедряване на събирането на боклука. LLVM се развива бързо, с голяма версия на всеки шест месеца или така. И темповете на развитие вероятно ще се засилят само благодарение на начина, по който много от сегашните езици поставят LLVM в основата на своя процес на развитие.