Что такое LLVM? Сила Swift, Rust, Clang и др.

Новые языки и улучшения существующих быстро растут по всему ландшафту разработки. Rust от Mozilla, Swift от Apple, Kotlin от Jetbrains и многие другие языки предоставляют разработчикам новые возможности выбора скорости, безопасности, удобства, портативности и мощности.

Почему сейчас? Одна из главных причин - новые инструменты для создания языков, в частности компиляторы. И главным из них является LLVM, проект с открытым исходным кодом, первоначально разработанный создателем языка Swift Крисом Латтнером в качестве исследовательского проекта в Университете Иллинойса.

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

Список языков, использующих LLVM, имеет много знакомых имен. Язык Apple Swift использует LLVM в качестве среды компиляции, а Rust использует LLVM в качестве основного компонента своей цепочки инструментов. Кроме того, у многих компиляторов есть версия LLVM, например, Clang, компилятор C / C ++ (это название «C-lang»), который сам по себе является проектом, тесно связанным с LLVM. Mono, реализация .NET, имеет возможность компилировать в машинный код с использованием серверной части LLVM. Kotlin, номинально являющийся языком JVM, разрабатывает версию языка под названием Kotlin Native, которая использует LLVM для компиляции в машинный код.

LLVM определен

По своей сути LLVM - это библиотека для программного создания машинного кода. Разработчик использует API для генерации инструкций в формате, который называется промежуточным представлением или IR. Затем LLVM может скомпилировать IR в автономный двоичный файл или выполнить JIT-компиляцию (точно в срок) кода для запуска в контексте другой программы, такой как интерпретатор или среда выполнения для языка.

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

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

Узнайте больше о Go, Kotlin, Python и Rust 

Идти:

  • Используйте всю мощь языка Google Go
  • Лучшие IDE и редакторы для языка Go

Котлин:

  • Что такое Котлин? Объяснение альтернативы Java
  • Kotlin frameworks: обзор инструментов разработки JVM

Python:

  • Что такое Python? Все, что Вам нужно знать
  • Учебник: как начать работу с Python
  • 6 основных библиотек для каждого разработчика Python

Ржавчина:

  • Что такое Rust? Способ делать безопасную, быструю и легкую разработку программного обеспечения
  • Узнайте, как начать работу с Rust 

LLVM: разработан для портативности

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

Напротив, LLVM IR с самого начала проектировался как портативная сборка. Одним из способов достижения этой переносимости является предложение примитивов, независимых от какой-либо конкретной машинной архитектуры. Например, целочисленные типы не ограничиваются максимальной разрядностью базового оборудования (например, 32 или 64 бита). Вы можете создавать примитивные целочисленные типы, используя столько битов, сколько необходимо, например 128-битное целое число. Вам также не нужно беспокоиться о создании вывода, соответствующего набору команд конкретного процессора; LLVM позаботится и об этом за вас.

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

Если вы хотите увидеть живые примеры LLVM IR, перейдите на сайт ELLCC Project и попробуйте живую демонстрацию, которая конвертирует C-код в LLVM IR прямо в браузере.

Как языки программирования используют LLVM

Наиболее распространенный вариант использования LLVM - это опережающий компилятор для языка. Например, проект Clang заблаговременно компилирует C и C ++ в собственные двоичные файлы. Но LLVM делает возможным и другие вещи.

Своевременная компиляция с LLVM

В некоторых ситуациях требуется, чтобы код создавался «на лету» во время выполнения, а не компилировался заранее. Например, язык Julia JIT-компилирует свой код, потому что он должен работать быстро и взаимодействовать с пользователем через REPL (цикл чтения-оценки-печати) или интерактивную подсказку. 

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 ++.
  • API 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 поддерживает собственный набор привязок к LLVM C API, но в настоящее время они не поддерживаются.
  • llvmpy, первая популярная привязка Python для LLVM, перестала обслуживаться в 2015 году. Плохо для любого программного проекта, но хуже при работе с LLVM, учитывая количество изменений, которые вносятся в каждую редакцию LLVM.
  • llvmcpy стремится обновить привязки Python для библиотеки C, обновлять их автоматически и сделать их доступными с использованием собственных идиом Python. llvmcpy все еще находится на ранней стадии, но уже может выполнять некоторую элементарную работу с API LLVM.

Если вам интересно, как использовать библиотеки LLVM для создания языка, у собственных создателей LLVM есть учебник, использующий C ++ или OCAML, который поможет вам создать простой язык под названием Kaleidoscope. С тех пор он был перенесен на другие языки:

  • Haskell:  прямой перенос исходного руководства.
  • Python: один такой порт внимательно следует руководству, а другой представляет собой более амбициозную переработку с интерактивной командной строкой. Оба они используют llvmlite в качестве привязки к LLVM.
  • Rust  и  Swift: казалось неизбежным, что мы получим портирование учебника на два из языков, которые LLVM помог создать.

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

Что не делает LLVM

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

Например, LLVM не анализирует грамматику языка. Многие инструменты уже выполняют эту работу, например lex / yacc, flex / bison, Lark и ANTLR. В любом случае синтаксический анализ должен быть отделен от компиляции, поэтому неудивительно, что LLVM не пытается решить эти проблемы.

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

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

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