Как уже знают все Android-разработчики, Google недавно объявила об официальной поддержке Kotlin в Android. Многие риски, связанные с использованием этого замечательного языка в Android-проектах, сняты. Но актуальным, особенно для очень крупных проектов, каким является Badoo, остаётся вопрос о скорости сборки. Я был рад обнаружить, что в сети уже есть исследования на эту тему, и переводом одного из них хочу поделиться.
Итак, если вы переводите приложение с Java на Kotlin, будет ли оно компилироваться дольше?
В более ранней статье обсуждалось конвертирование Android-приложения из Java целиком в Kotlin. Кода на Kotlin получалось меньше, и он был удобнее в сопровождении, чем на Java, так что я пришёл к выводу, что оно того стоило. Но некоторые разработчики не хотят пробовать Kotlin, опасаясь, что он может компилироваться медленнее Java. И это беспокойство – справедливо: никто не хочет тратить время на конвертирование кода, если в результате сборка будет длиться дольше. Так что давайте изучим длительность компиляции приложения App Lock до и после конвертирования в Kotlin. Я не буду сравнивать скорость Kotlin и Java построчно, а вместо этого попытаюсь ответить на вопрос, повлияет ли конвертирование всей кодовой базы из одного языка в другой на общую продолжительность сборки.
Как я тестировал длительность сборки
Я написал shell-скрипты для повторяемых запусков Gradle-сборок по разным сценариям. Все тесты выполнялись последовательно по десять раз. Перед каждым новым сценарием проект очищался. Для сценариев, использующих демона Gradle, последний останавливался перед запуском бенчмарка.
Все бенчмарки выполнялись на машине с Intel Core i7–6700, работающим с частотой 3,4 ГГц, оснащённой 32 Гб памяти DDR4, а также SSD-приводом Samsung 850 Pro. Исходный код собирался с помощью Gradle 2.14.1.
Тесты
Я хотел прогнать бенчмарки для нескольких распространённых сценариев использования: чистые сборки с/ без демона Gradle, инкрементальные сборки без изменения файлов, инкрементальные сборки с изменённым файлом.
Кодовая база App Lock на Java содержала 5491 метод и 12 371 строку кода. После конвертирования в Kotlin количество методов уменьшилось до 4987, а количество строк – до 8564. В процессе преобразования в архитектуру не вносились никакие серьёзные изменения, так что измерение длительности компиляции до и после конвертирования должно дать чёткое представление о разнице в продолжительности сборки между Java и Kotlin.
Чистые сборки без демона Gradle
Это наихудший сценарий с точки зрения продолжительности сборки для обоих языков: запуск чистой сборки с холодным стартом. Для этого теста я отключил демон Gradle.
Вот сколько времени заняли все десять сборок:
Десять последовательных чистых сборок без демона Gradle
Средняя продолжительность сборки Java составляет 15,5 секунд, Kotlin – 18,5 секунд: увеличение на 17%. Не лучшее начало для Kotlin, но большинство людей компилируют свой код по другим сценариям.
Чаще всего мы несколько раз компилируем одну и ту же кодовую базу по мере внесения в неё изменений. Именно для этого сценария был разработан демон Gradle, так что давайте включим его и посмотрим, что получится.
Чистые сборки с включённым демоном Gradle
Одной из проблем JIT-компиляторов вроде JVM является то, что они тратят время на компиляцию исполняемого в них кода, так что по мере его исполнения производительность процесса увеличивается. Но если остановить JVM-процесс, то прирост производительности теряется. При каждой сборке Java-кода обычно приходится запускать и останавливать JVM. В результате он каждый раз заново делает одну и ту же работу. Для решения этой проблемы Gradle поставляется с демоном, который продолжает функционировать между сборками и помогает поддерживать прирост производительности, обеспечиваемый JIT-компиляцией. Включить демон можно с помощью Gradle-команды --daemon
, вводимой в командной строке, или с помощью добавления org.gradle.daemon=true
в файл gradle.properties
.
Вот результат прогона той же серии сборок, но с включённым демоном Gradle:
Десять последовательных сборок с включённым демоном Gradle
Как видите, первый прогон занимает примерно столько же времени, сколько в сценарии без демона. В последующих сборках производительность растёт вплоть до четвёртого прогона. При таком сценарии более целесообразно оценивать среднюю продолжительность сборки после третьего прогона, когда демон уже прогрелся. В этом случае чистая сборка на Java занимает в среднем 14,1 секунды, а на Kotlin – 16,5 секунд: увеличение на 13%.
Kotlin догоняет Java, но всё ещё отстаёт. Тем не менее вне зависимости от используемого языка демон Gradle уменьшает длительность сборок более чем на 40%. Если вы его ещё не используете, то самое время начать.
Итак, полные сборки на Kotlin выполняются чуть медленнее, чем на Java. Но обычно мы компилируем после внесения изменений всего лишь в несколько строк кода, так что инкрементальные сборки должны демонстрировать другую производительность. Давайте узнаем, сможет ли Kotlin догнать Java там, где это важно.
Инкрементальные сборки
Использование инкрементальной компиляции является одним из важнейших свойств компилятора по повышению производительности. При обычной сборке перекомпилируются все исходные файлы проекта, а при инкрементальной – отслеживается, какие файлы изменились с момента предыдущей сборки, и в результате перекомпилируются только эти файлы и те, что от них зависят. Это может оказывать очень сильное влияние на длительность компиляции, особенно в больших проектах.
Инкрементальные сборки появились в Kotlin 1.0.2, их можно включить, добавив kotlin.incremental=true
в файл gradle.properties
, или через командную строку.
Итак, как изменится длительность компиляции Kotlin по сравнению с Java при использовании инкрементальной компиляции?
Вот результаты бенчмарка при условии отсутствия изменений в файлах:
Десять последовательных инкрементальных сборок без изменения файлов
Теперь протестируем инкрементальную компиляцию при условии изменения одного исходного файла. Для этого я перед каждой сборкой изменял Java-файл и его Kotlin-эквивалент. В данном бенчмарке это файл, относящийся к пользовательскому интерфейсу, от него не зависят другие файлы:
Десять последовательных инкрементальных сборок с одним отдельным изменённым файлом
Наконец, давайте посмотрим на результаты инкрементальной компиляции с одним изменённым исходным файлом, который импортируется во многие другие файлы проекта:
Десять последовательных инкрементальных сборок при условии изменения одного ключевого файла
Как видите, демону Gradle всё ещё приходится прогревать в течение двух–трёх прогонов, но после этого оба языка становятся очень близки по производительности. При отсутствии изменений в файлах у Java уходит 4,6 секунды на прогретую сборку, а у Kotlin – 4,5 секунды. Если мы меняем файл, но он не используется другими файлами, то Java требуется 7 секунд на выполнение прогретой сборки, а Kotlin – 6,1 секунды. Наконец, если изменённый файл импортируется во многие другие файлы проекта, то при прогретом демоне Gradle инкрементальная сборка Java занимает 7,1 секунды, а у Kotlin уходит в среднем 6 секунд.
Заключение
Мы измерили производительность при нескольких разных сценариях, чтобы узнать, сможет ли Kotlin конкурировать с Java по длительности компиляции. При чистых сборках, которые выполняются сравнительно редко, Java превосходит Kotlin на 10–15%. Но чаще всего разработчики выполняют частичные сборки, при которых большой выигрыш во времени достигается за счёт инкрементального компилирования. Благодаря работающему демону Gradle и включённой инкрементальной компиляции Kotlin не уступает, или даже немного превосходит Java. Впечатляющий результат, которого я не ожидал. Выражаю команде разработчиков Kotlin своё почтение за создание языка, который не только обладает прекрасными возможностями, но и компилируется так быстро.
Если вы пока не попробовали Kotlin из опасений увеличения длительности компиляции, то можете больше не беспокоиться: он компилируется так же быстро, как Java.
Сырые данные, собранные мной при прогоне бенчмарков, лежат здесь.
Автор: ArkadyGamza