Малоизвестный исследовательский проект, который может значительно ускорить инновации в проектировании языков программирования
От переводчика
Хочу сразу предупредить, что статья местами напоминает презентацию крупной компании из-за эпитетов в духе «изменит индустрию», «лучший на рынке», «прорывные технологии» и др. Если закрыть глаза на такой эмоциональный стиль повествования, то получится интересная вводная статья про новинки технологий компиляторов и виртуальных машин.
Введение
Со времён расцвета компьютерной индустрии многие были увлечены квестом в поисках идеального языка программирования. Квест очень сложный: создание нового языка — задача не из лёгких. И очень часто в процессе происходит дробление сложившейся экосистемы программирования и возникает необходимость заново строить базовые инструменты для нового языка: компилятор, отладчик, HTTP стек, IDE, библиотеки и бесконечное число базовых блоков пишутся с нуля для каждого нового языка. Совершенство в дизайне языков программирования недостижимо, и новые идеи возникают постоянно. Мы похожи на Сизифа: приговоренного богами на вечное толкание камня в гору, чтобы в итоге увидеть, как тот скатывается вниз снова и снова … целую вечность.
Как можно разорвать этот порочный цикл? Давайте помечтаем, чего бы нам хотелось.
Нам нужно нечто, специальный инструмент, который позволит сделать следующее:
- Способ создать новый язык всего за неделю
- И чтобы он автоматически работал так же быстро, как другие языки
- Чтобы у него была поддержка качественного отладчика, автоматически (в идеале без замедления работы программы)
- Поддержка профилирования, автоматически
- Наличие качественного сборщика мусора, автоматически … но только, если он нам понадобится
- Чтобы язык мог использовать весь существующий код, независимо от того, на чём он был написан
- Чтобы язык поддерживал любой стиль программирования от низкоуровневого C или FORTRAN до Java, Haskell и полностью динамических скриптовых языков, таких как Python и Ruby.
Чтобы поддерживал just-in-time и ahead-of-time компиляцию - И наконец, чтобы поддерживалась горячая замена кода (hotswap) в уже работающей программе.
А ещё, мы, конечно, хотим иметь открытый исходный код этого волшебного инструмента. И пусть там будут пони. Вроде бы всё.
Умный читатель конечно же догадался, что я не стал бы начинать эту статью, если бы у меня не было такого инструмента. Зовут его немного странно — Graal & Truffle. И пусть название скорее подходит к претенциозному хипстерскому ресторану, по сути это значительный исследовательский проект, в котором участвуют более 40 учёных из IT индустрии и университетов. Вместе они строят новые технологии компиляторов и виртуальных машин, которые реализуют все пункты из нашего списка.
Они изобрели новый способ быстрого создания языков программирования без проблем с библиотеками, оптимизирующими компиляторами, отладчиками, профилировщиками, привязки к C библиотекам и остальными атрибутами, которые необходимы современному языку программирования. Этот инструмент обещает вызвать волну новых инноваций в языках программирования и — я надеюсь — переформатирует всю IT индустрию.
Вот об этом и поговорим.
Что такое Graal и Truffle?
Graal — это исследовательский компилятор. А Truffle — это … это такая штука, которую сложно с чем-нибудь сравнить. Если вкратце, то на ум приходит следующее: Truffle — это фреймворк для создания языков программирования через написание интерпретатора для абстрактного синтаксического дерева.
При создании нового языка программирования первый делом определяется грамматика. Грамматика — это описание синтаксических правил вашего языка. Используя грамматику и инструмент наподобие ANTLR вы получите парсер. На выходе парсера вы будете иметь дерево парсинга
На картинке изображено дерево, которое получено после работы ANTLR для следующей строчки кода:
Abishek AND (country = India OR City = BLR) LOGIN 404 | show name
Дерево парсинга и производная из неё конструкция под названием абстрактное синтаксическое дерево (AST) — это естественный способ выражения программ. После построения такого дерева останется один простой шаг до работающей программы. Вам нужно будет добавить метод «execute» в класс узла дерева. Каждый узел при выполнении «execute» может вызывать дочерние узлы и комбинировать полученные результаты для получения значения выражения или выполнения инструкции. И, в принципе, это всё!
Возьмём для примера динамические языки программирования, такие как Python, JavaScript, PHP и Ruby. Для этих языков их динамизм стал результатом движения по пути наименьшего сопротивления. Если вы создаёте язык с нуля, то усложнение языка статической системой типов или оптимизирующим компилятором может сильно замедлить разработку языка. Последствия такого выбора выливаются в низкой производительности. Но что ещё хуже, возникает соблазн быстро добавлять фичи в простой/медленный интерпретатор AST. Ведь оптимизировать производительность этих фич впоследствии будет очень сложно.
Truffle — это фремворк для написания интерпретаторов с использованием аннотаций и небольшого количество дополнительного кода. Truffle в паре с Graal позволит конвертировать такие интерпретаторы в JIT компилирующие виртуальные машины (VM) … автоматически. Полученная среда исполнения в моменты пиковой производительности может соревноваться с лучшими существующими компиляторами (настроенными вручную и заточенными под конкретный язык). Например полученная таким образом реализация языка JavaScript под названием TruffleJS может тягаться с V8 в тестах производительности.
Движок RubyTruffle быстрее чем все остальные реализации Ruby. Движок TruffleC примерно соревнуется с GCC. Уже существуют реализации некоторых языков (разной степени готовности) с использованием Truffle:
- JavaScript
- Python 3
- Ruby
- LLVM bitcode — позволяет запускать программы на C/C++/Objective-C/Swift
- Ещё движок для интерпретирования исходного кода на C без компиляции его в LLVM (о преимуществах такого подхода читайте ниже)
- R
- Smalltalk
- Lua
- Множество маленьких экспериментальных языков
Чтобы вам стало понятно, насколько просто создавать эти движки, исходные код TruffleJS занимает всего около 80 000 строк, по сравнению с 1.7 миллионами строк кода в V8.
К чёрту подробности, как мне поиграться со всем этим?
Graal и Truffle — результат работы Oracle Labs, части Java команды, работающей над исследованиями в области виртуальных машин. GraalVM лежит здесь. Это расширенный Java Development Kit, который содержит в себе некоторые языки из указанного выше списка, а также заменители консольных утилит NodeJS, Ruby и R. В поставку также входит учебный язык программирования «SimpleLanguage», с которого можно начать знакомство с Graal и Truffle.
Для чего нужен Graal?
Если Truffle — это фреймворк для создания AST интерпретаторов, то Graal — это штука для ускорения таких интерпретаторов. Graal — это произведение искусства в области оптимизирующих компиляторов. Он поддерживает следующие фичи:
- может запускаться в режимах just-in-time и ahead-of-time
- очень продвинутые оптимизации, включающие partial escape analysis. Escape analysis позволяет избежать аллокации объектов в куче, если это по сути не нужно. Популярность EA обязана развитию JVM, но эта оптимизация очень сложная, и поддерживается малым количество виртуальных машин. JavaScript компилятор «Turbofan», используемый в браузере Chrome, начал получать escape analysis оптимизации только в конце 2015 года. В то время, как у Graal есть продвинутые оптимизации для более широкого спектра случаев.
- Работает в связке с языками на основе Truffle и позволяет конвертировать Truffle AST в оптимизированный нативный код благодаря частичному вычислению. Частичное вычисление специализированного интерпретатора называется «первая проекция Футамуры» («first Futamura projection»). [Насколько Я понял из Википедии, первая проекция Футамуры позволяет подгонять интерпретатор под исходный код. То есть, если в коде не используются некоторые фичи языка, то эти фичи “выпиливаются” из интерпретатора. А затем интерпретатор «превращается» в компилятор, генерирующий нативный код. Поправьте, если Я напутал.]
- Содержит расширенные инструменты визуализации, позволяющие изучать промежуточное представление компилятора на каждом этапе оптимизаций.
- Написан на Java. А значит его гораздо проще изучать и экспериментировать, чем традиционные компиляторы, написанные на C или C++.
- Начиная с Java 9, Graal можно использовать как JVM плагин.
Визуализация IGV
С самого начала Graal проектировался как мультиязыковой компилятор. Но его оптимизации особенно хорошо подходят для языков высокого уровня абстракции и динамизма. Java программы работают в этой виртуальной машине так же, как в существующих JVM компиляторах. В то время, как Scala программы работают приблизительно на 20% быстрее. Ruby программы получают прирост 400% по сравнению с лучшей на сегодняшний день реализацией (и это не MRI).
Polyglot
Ещё не устали? А ведь это только начало.
Truffle содержит специальный фреймворк для межъязыкового взаимодействия под названием «Polyglot». Он позволяет Truffle-языкам вызывать друг к друга. А ещё есть Truffle Object Storage Model, которая стандартизирует и оптимизирует большую часть поведения объектов в динамически типизированных языках. А заодно позволяет делиться такими объектами. Не забывайте, что Graal и Truffle построены на технологии Java и могут общаться с JVM языками, такими как Java, Scala и даже Kotlin. Причем это общение двустороннее: Graal-Truffle код может вызывать Java библиотеки, а Java код — вызывать Graal-Truffle библиотеки.
Polyglot работает очень необычным способом. Как вы помните, Truffle представляет из себя фреймворк для описания узлов абстрактного синтаксического дерева (AST). В результате обращение к другим языкам происходит не через дополнительный слой абстракций и обёрток, а путём слияния двух AST деревьев в одно. В результате полученное синтаксическое дерево компилируется (и оптимизируется) внутри Graal как единое целое. А значит любые сложности, которые могут появится на стыке двух языков программирования, могут быть проанализированы и упрощены.
По этой причине исследователи решили реализовать интерпретатор C через Truffle. Обычно мы воспринимает C как компилируемый язык. Но реальных причин для этого нет. История знает случаи, когда интерпретатор C использовался в реальных программах. Например видеоредактор Shake special effect давал пользователям возможность писать скрипты на C.
Из-за известных проблем с производительность скриптовых языков, критические участки программ часто переписывают на C с использованием внутренних API интерпретатора. В итоге, такой подход заметно усложняет задачу ускорения языка, ведь помимо самого интерпретатора необходимо запускать расширения на C. Задача глобальной оптимизации усложняется из-за того, что эти расширения опираются на допущения о внутренней реализации языка.
Когда авторы RubyTruffle столкнулись с этой проблемой, они пришли к изящному решению: давайте напишем специальный интерпретатор C, который будет не только понимать простой C, но также макросы и другие конструкции, специфичные для Ruby расширений. Тогда после слияния интерпретаторов Ruby и C через Truffle получится единый код, а издержки межъязыкового общения будут нивелированы оптимизатором. В результате получился TriffleC.
Можете почитать прекрасное объяснение как всё это работает в статье одного из исследователей этого проекта Криса Ситона (Chris Seaton). Или можете изучить научную статью с подробным описанием.
Давайте сделаем безопасное управление памятью в C
Программы на C работают быстро. Но есть обратная сторона медали — их очень любят хакеры, потому что слишком просто выстрелить себе в ногу во время работы с памятью.
Язык ManagedC появился как продолжение TruffleC и заменяет стандартное управление памятью на контролируемые аллокации со сборщиком мусора. ManagedC поддерживает арифметику указателей и другие низкоуровневые конструкции, при этом избегая кучи ошибок. Цена надёжности — 15% проседание производительности по сравнению с GCC, а также активное использование «неопределённого поведения», которое так любят многие компиляторы C. Это значит, что если ваша программа работает с GCC, это не гарантирует, что она запустится под ManagedC. При этом сам ManagedC полностью реализует стандарт C99.
Вы можете почерпнуть больше сведений из статьи «Memory safe execution of C on a Java VM».
Отладка и профилирование на халяву
Все разработчики языков программирования сталкиваются с проблемой нехватки качественных инструментов. Возьмём для примера GoLang. Экосистема GoLang уже много лет страдает от плохих, примитивных и плохо портируемых отладчика и профилировщика.
Ещё одна проблема — добавить поддержку отладчика в интерпретатор. То есть нативный код должен быть близок к исходному коду, чтобы можно быть установить взаимно однозначное соответствие между текущим состоянием виртуальной машины и того кода, что написал разработчик. Обычно это приводит к отказу от оптимизаций компилятора и общему замедлению отладки.
И тут приходит на помощь Truffle, который предлагает простой API, позволяющий встроить продвинутый отладчик в интерпретатор … без замедления программы. Компилятор всё также применяет оптимизации, а состояние программы при отладке остаётся таким, как того ожидает программист. Всё благодаря метаданным, которые генерируют Graal и Truffle в процессе компиляции в нативный код. Полученные метаданные затем используются для деоптимизации участков запущенной программы, чтобы получить исходное состояние интерпретатора. При использовании точки остановки (breakpoint), точки наблюдения (watchpoint), точки профилирования (profiling point) или другого отладочного механизма виртуальная машина откатывает программу к её медленной форме, добавляет в AST ноды для реализации нужного функционала и перекомпилирует всё обратно в нативный код, который подменяется на лету.
Конечно отладчик это не только фича среды исполнения. Нужен ещё UI для пользователей. Поэтому есть плагин для NetBeans IDE с поддержкой любого Truffle языка.
За подробностями отправляю вас к статье «Building debuggers and other tools: we can have it all».
Поддержка LLVM
Truffle в основном используется для создания интерпретаторов исходного кода. Но ничто не мешает использовать этот фреймворк для других целей. Проект Sulong — это Truffle интерпретатор для байткода LLVM.
Проект Sulong пока находится на ранней стадии разработки и содержит ряд ограничений. Но теоретически запуск байткода с помощью Graal и Truffle позволит работать не только с C, но также C++, Objective-C, FORTRAN, Swift и возможно даже Rust.
Sulong также содержит простой C API для взаимодействия с другими Truffle языками через Polyglot. Опять же, благодаря независимости от языка программирования и полностью динамичной природе данного API, полученный код будет агрессивно оптимизирован, а накладные расходы будут сведены к минимуму.
Горячая замена (HotSwap)
Горячая замена — это возможность подменить программный код в процессе его работы без перезапуска. Это одно из главных преимуществ динамических языков программирования, позволяющее добиться высокой производительности программистов. Есть научная статья посвящённая реализации горячей замены в фреймвороке Truffle (не уверен, что поддержка уже добавлена в сам Truffle). Как и с отладчиком, профилировщиком и оптимизаторами, разработчикам языков на Truffle нужно будут просто использовать новые API для поддержки горячей замены. Этот API гораздо удобнее чем собственные велосипеды.
Где подвох?
Как известно, в жизни нет ничего идеального. Graal и Truffle предлагают решение для почти всех наших хотелок из первоначального списка. Разработчики языков программирования должны быть просто счастливы. Но есть цена такому удобству:
- Время прогрева
- Потребление памяти
Процесс конвертации интерпретатора в оптимизированный нативный код требует изучения того, как программа работает в реальных условиях. Это, конечно, не новость. Термин «прогрев» (т.е. ускорение по мере работы) известен всем, кто использует современные виртуальные машины, такие как HotSpot или V8. Но Graal и здесь двигает прогресс, выводя оптимизации на основе профилирование на более высокий уровень. В итоге GraalVM очень сильно зависит от профилировочной информации.
Именно по этой причине исследователи замеряют только пиковую производительность, то есть дают программе поработать некоторое время. При этом не учитывается время, необходимое на такой прогрев. Для серверных приложений это не представляет большой проблемы, так как в этой области важна лишь пиковая производительность. Но в других приложениях долгий прогрев может поставить крест на использовании Graal. На практике такую картину можно увидеть при работе с тестом производительности Octane (вы можете найти его в техническом превью JDK): итоговый балл немного меньше чем у Chrome, даже с учётом довольно долгого прогрева Graal (15-60 секунд), который не учитывается в итоговой оценке.
Вторая проблема: потребление памяти. Если программа сильно зависит от спекулятивных оптимизаций, ей необходимо хранить таблицы с метаинформацией компилятора для деоптимизации — обратного преобразования из состояния машины к абстрактному интерпретатору. И занимает эта метаинформация столько же места, сколько итоговый код, даже с учетом всех уплотнений и сжатий. Не забывайте, что нужно хранить исходное AST или байткод на случай нарушения эвристических предположений в нативном коде. Всё это нещадно загромождает RAM.
Добавьте к этому тот факт, что Graal, Truffle и языки на основе Truffle сами написаны на Java. А значит компилятору тоже нужно время на свой прогрев, чтобы работать в полную силу. И конечно же потребление памяти для базовых структур данных будет расти, и эти базовые структуры компилятора попадают под сборку мусора.
Люди, стоящие за Graal и Truffle, конечно в курсе этих проблем и уже обдумывают пути решения. Одно из них называется SubstrateVM. Это виртуальная машина целиком написанная на Java и скомпилированная заранее (ahead-of-time) используя соответствующий компилятор Graal и Truffle. Функционально SubstrateVM не такая продвинутая как HotSpot: она не может динамически подгружать код из интернета, и сборщик мусора у неё довольно простой. Но проделав компиляцию этой виртуальной машины один раз можно сильно сэкономить на времени прогрева в будущем.
И ещё одна тонкость, о которой нельзя умолчать. В нашем первоначальном списке хотелок есть пункт об открытости исходного кода. Graal и Trulle — это крупные и очень дорогие проекты, написанные опытными людьми. Поэтому их разработка не может быть дешёвой. На сегодняшний день только некоторые части из описанного распространяются с открытыми исходниками.
Весь код можно найти на GitHub или других зеркалах:
- Graal & Truffle.
- Расширяемая версия HotSpot (основа проекта).
- RubyTruffle
- Sulong (поддержка LLVM bitcode)
- Реализации R, Python 3 и Lua (некоторые из них созданы как хобби/исследовательские проекты).
А следующие части не относятся к OpenSource
- TruffleC/ManagedC
- TruffleJS/NodeJS API
- SubstrateVM
- Поддержка AOT
TruffleJS можно бесплатно скачать в составе GraalVM preview release. Я не знаю, как можно поиграться с TruffleC или ManagedC. Проект Sulong покрывается часть этой функциональности.
Что почитать
Каноничный, полномасштабный, всё-в-одном туториал по всему Graal и Truffle в этом докладе: «One VM to rule them all, one VM to bind them». Он идёт три часа, я вас предупредил. Только для настоящих энтузиастов.
Есть ещё туториалы по использованию Truffle:
Что дальше?
В начале я упомянул, что мы можем убрать барьеры на пути создания новых языков программирования. Это откроет двери на для новой волны инноваций в языках. Вот небольшой список экспериментальных языков. Надеюсь этот список будет пополняться в будущем.
Если вы попробуете новые идеи, предлагаемые в Graal и Truffle, то откроете для себя возможность реально работать с собственным языком с первых дней его существования. Как результат, возникнет растущее сообщество участников, которые смогут развернуть ваш язык в своих проектах. Это породит магический цикл: сообщество оставляет свои отзывы и идеи, вы внедряете улучшения. В целом это ускорит путь от экспериментов до конечного внедрения. Это как раз то, что я ожидаю.
Автор: sheknitrtch