Хабрахабр

Компилируем Kotlin: JetBrains VS ANTLR VS JavaCC


Насколько быстро парсится Kotlin и какое это имеет значение? JavaCC или ANTLR? Годятся ли исходники от JetBrains?

Сравниваем, фантазируем и удивляемся.

JetBrains слишком тяжело таскать за собой, ANTLR хайповый, но неожиданно медленный, а JavaCC ещё рано списывать.

Парсинг простого Kotlin файла тремя разными реализациями:

Имплементация

Первый запуск

1000й запуск

размер джара (парсера)

JetBrains

3254мс

16,6мс

35.3МБ

JetBrains (w/o analyzer)

1423мс

0,9мс

35.3МБ

ANTLR

3705мс

137,2мс

15.5МБ

JavaCC

19мс

0,1мс

1.6МБ

Одним погожим солнечным деньком...

я решил собрать транслятор в GLSL из какого-нибудь удобного языка. Идея была в том, чтобы программировать шейдеры прямо в идее и получить «бесплатно» поддержку IDE — синтаксис, дебаг и юнит-тесты. Получилось действительно очень удобно.

Кроме того — он хайповый. С тех пор осталась идея использовать Kotlin — в нём можно использовать имя vec3, он более строг и в IDE с ним удобнее. Хотя с точки зрения моего внутреннего менеджера это всё недостаточные причины, но идея столько раз возвращалась, что я решил от неё избавиться просто реализовав.

Нет перегрузки операторов, поэтому синтаксис арифметики векторов будет уж слишком отличаться от того что вы привыкли видеть в геймдеве Почему не Java?

JetBrains

Ребята из JetBrains выложили код своего компилятора на гитхаб. Как им пользоваться можно подсмотреть тут и тут.

Тут тип для читателя очевиден, но в AST эту информацию не так просто получить, особенно когда справа другая переменная, или вызов функции. Сначала я использовал их парсер вместе с анализатором, потому что для трансляции в другой язык — необходимо знать какой тип у переменной без явного указания типа val x = vec3().

Первый запуск парсера на примитивном файле занимает 3с (ТРИ СЕКУНДЫ). Здесь меня постигло разочарование.

482ms
min time in next 10 calls: 70. Kotlin JetBrains parser
first call elapsed : 3254. 973ms
min time in next 1000 calls: 16. 071ms
min time in next 100 calls: 29. 888756 seconds

Такое время имеет следующие очевидные неудобства: 655ms
Whole time for 1111 calls: 40.

  1. потому что это плюс три секунды к запуску игры или приложения.
  2. во время разработки я использую горячую перегрузку шейдера и вижу результат сразу после изменения кода.
  3. я часто перезапускаю приложение и рад что оно стартует достаточно быстро (секунда-две).

Плюс три секунды на разогрев парсера — это неприемлемо. Конечно, сразу выяснилось что при последующих вызовах время парсинга падает до 50мс и даже до 20мс, что убирает (почти) из выражения неудобство №2. Но два остальных никуда не деваются. К тому же, 50мс на файл — это плюс 2500мс на 50 файлов (один шейдер — это 1-2 файла). А если это Android? (Тут мы пока говорим только про время.)

Время парсинга простого файла падает с 70мс до 16мс. Обращает на себя внимание сумасшедшая работа JIT. Что означает, во первых — сам JIT потребляет ресурсы, а во вторых — на другой JVM результат может сильно отличаться.

Ведь мне нужно всего лишь расставить типы и сделать это можно относительно легко, в то время как JetBrains анализатор делает что-то гораздо более сложное и собирает гораздо больше информации. В попытке выяснить откуда такие цифры, нашёлся вариант — использовать их парсер без анализатора. 9мс где-то к тысяче. И тогда время запуска падает в два раза (но почти полторы секунды это всё равно прилично), а время последующих вызовов уже гораздо интереснее — с 8мс в первых десяти, до 0.

731ms
min time in next 10 calls: 8. Kotlin JetBrains parser (without analyzer) (исходник)
first call elapsed : 1423. 323ms
min time in next 1000 calls: 0. 275ms
min time in next 100 calls: 2. 6884801 seconds

Пришлось собирать именно такие цифры. 974ms
Whole time for 1111 calls: 3. Оно критично, потому что тут не отвлечёшь пользователя пока шейдера грузятся в фоне, он просто ждёт. Время первого запуска важно при прогрузке первых шейдеров. Но раз отказ от него становится обсуждаемым вариантом, можно попробовать использовать и другие парсеры. Падение времени исполнения важно чтобы видеть саму динамику, как работает JIT, насколько эффективно мы можем прогружать шейдера на разогревшемся приложении.

Главной причиной посмотреть в первую очередь на JetBrains парсер — было желание использовать их типизатор. Кроме того, не-JetBrains скорее всего будет гораздо меньше по размеру, менее требователен к окружению, проще с поддержкой и включением кода в проект.

ANTLR

На JavaCC парсера не нашлось, а вот на хайповом ANTLR, ожидаемо, есть.

Те же 3с на прогрузку (первый вызов) и фантастические 140мс на последующие вызовы. Но вот что было неожиданно — так это скорость. Видимо, ребята из JetBrains, сделали какую-то магию, позволив JIT так оптимизировать их код. Тут уже не только первый запуск длится неприятно долго, но и потом ситуация не исправляется. Потому что ANTLR совсем не оптимизируется со временем.

101ms
min time in next 10 calls: 139. Kotlin ANTLR parser (исходник)
first call elapsed : 3705. 279ms
min time in next 1000 calls: 137. 596ms
min time in next 100 calls: 138. 90619 seconds

20099ms
Whole time for 1111 calls: 161.

JavaCC

В общем, с удивлением отказываемся от услуг ANTLR. Парсинг не должен быть таким долгим! В грамматике Котлина нет каких-то космических неоднозначностей, да и проверял я его на практически пустых файлах. Значит, настало время расчехлить старичка JavaCC, закатать рукава, и всё таки «сделать самому и как надо».

На этот раз цифры получились ожидаемыми, хотя в сравнении с альтернативами — неожиданно приятными.

024ms
min time in next 10 calls: 1. Kotlin JavaCC parser (исходник)
first call elapsed : 19. 379ms
min time in next 1000 calls: 0. 952ms
min time in next 100 calls: 0. 38707677 seconds

Внезапные плюсы своего парсера на JavaCC
Конечно, вместо написания своего парсера хотелось бы использовать готовое решение. 114ms
Whole time for 1111 calls: 0. Но существующие имеют огромные недостатки:

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

В то время как получившийся парсер на JavaCC это

+ отличная скорость и на старте и в процессе
+ всего несколько классов самого парсера

Выводы

JetBrains слишком тяжело таскать за собой, ANTLR хайповый, но неожиданно медленный, а JavaCC ещё рано списывать.

Парсинг простого Kotlin файла тремя разными реализациями:

Имплементация

Первый запуск

1000й запуск

размер джара (парсера)

JetBrains

3254мс

16,6мс

35.3МБ

JetBrains (w/o analyzer)

1423мс

0,9мс

35.3МБ

ANTLR

3705мс

137,2мс

15.5МБ

JavaCC

19мс

0,1мс

1.6МБ

В какой-то момент, я решил посмотреть на размер джара со всеми зависимостями. JetBrains велик вполне ожидаемо, а вот рантайм ANTLR удивляет своим размером.

Но и для десктопа имеет значение, т.к., фактически, означает количество дополнительного кода, в котором могут водиться баги, который должна индексировать IDE, который, как раз, и влияет на скорость первой загрузки и скорость разогрева. Размер джара как таковой важен, конечно, для мобилок. Кроме того, для сложного кода нет особой надежды транслировать на другой язык.
Я не призываю считать килобайты и ценю время программиста и удобство, но всё же об экономии стоит задумываться, потому что именно так проекты и становятся неповоротливыми и трудно поддерживаемыми.

Ещё пара слов об ANTLR и JavaCC

Это было бы хорошо, если бы за это не нужно было так дорого платить. Серьёзной фичей ANTLR является разделение грамматики и кода. Плюс, если мы сэкономим и возьмём “стороннюю” грамматику — она может быть просто неудобна, в ней всё равно нужно будет досконально разбираться, преобразовывать дерево под себя. Да и значение это имеет только для “серийных разработчиков грамматик”, а для конечных продуктов это не так важно, ведь даже существующую грамматику всё равно придётся прошерстить чтобы написать свой код. В общем, JavaCC, конечно, смешивает мух и котлеты, но такое ли большое это имеет значение и так ли это плохо?

Но тут посмотреть можно с другой стороны — код из под JavaCC очень простой. Ещё одной фишкой ANTLR является множество target платформ. Прямо с вашим кастомным кодом — хоть в C#, хоть в JS. И его очень просто… транслировать!

P.S.

Весь код находится тут github.com/kravchik/yast

Но YastNode — это не совсем «сферическая нода в вакууме». Результатом парсинга у меня является дерево построенное на YastNode (это очень простой класс, фактически — мапа с удобными методами и айдишником). Именно этим классом я активно пользуюсь, на его основе у меня собрано несколько инструментов — типизатор, несколько трансляторов и оптимизатор/инлайнер.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть