Почему Kotlin иногда тормозит, и как это исправить?
Вы написали элегантный код на Kotlin. Он лаконичный, безопасный и — внезапно — тормозит. Знакомая ситуация? Kotlin — мощный язык, который работает поверх JVM, но его синтаксический сахар и фичи вроде inline или sequence могут как ускорить, так и замедлить приложение. В этой статье мы разберем, как профилировать Kotlin и что делать с результатами, чтобы ваш код летал.
Мы не будем переписывать руководства по Java-профилировщикам. Вместо этого сфокусируемся на специфике оптимизации Kotlin: от лямбд и коллекций до корутин и inline-функций. Поехали!
Инструменты для профилирования Kotlin
Прежде чем оптимизировать, нужно измерить. Вот три рабочих инструмента, которые стоит добавить в арсенал.
1. IntelliJ Profiler (встроенный)
Самый простой способ — запустить профилировщик прямо из IDE. Он показывает CPU, память и потоки. Отлично подходит для поиска "горячих точек" в Kotlin-коде.
2. JMH (Java Microbenchmark Harness)
Для микробенчмарков. Kotlin-код компилируется в байткод JVM, поэтому JMH работает идеально. Это ваш выбор, если нужно сравнить производительность listOf против sequence.
3. Android Studio Profiler
Для мобильных приложений. Показывает утечки памяти и частоту GC. Kotlin-код на Android часто страдает от создания лишних объектов из-за лямбд.
Важно: не оптимизируйте вслепую. Сначала профилируйте, потом — правьте. Только так производительность Kotlin станет предсказуемой.
5 реальных примеров оптимизации Kotlin-кода
Теория хороша, но давайте к практике. Вот пять типичных сценариев, где Kotlin может "тормозить", и способы это исправить.
1. Inline-функции: когда синтаксический сахар вредит
Kotlin поощряет функциональный стиль. Но каждая лямбда — это объект. Если вы используете filter{}.map{} в цикле, создаются десятки анонимных классов.
// Плохо: создает объект лямбды на каждой итерации
fun processItems(items: List<Int>) {
items.forEach { item ->
if (item % 2 == 0) doSomething(item)
}
}
// Хорошо: inline + обычный цикл
inline fun processItemsFast(items: List<Int>) {
for (item in items) {
if (item % 2 == 0) doSomething(item)
}
}
Используйте inline для функций высшего порядка, которые вызываются часто. Но не злоупотребляйте — слишком большой inline-код раздувает байткод.
2. Последовательности (Sequence) против List
Цепочки из filter, map и sorted на коллекциях создают промежуточные списки. Sequence работает лениво и экономит память.
// List: создает 3 промежуточных списка
val result = list
.filter { it > 10 }
.map { it * 2 }
.take(5)
// Sequence: вычисляется лениво, один проход
val resultSeq = list.asSequence()
.filter { it > 10 }
.map { it * 2 }
.take(5)
.toList()
Для больших коллекций (10k+ элементов) sequence выигрывает по памяти и скорости. Для маленьких — list быстрее из-за накладных расходов на обертку.
3. Корутины: Dispatchers и контекст
Корутины — мощь Kotlin, но неправильный диспетчер убивает производительность. Dispatchers.IO для сети, Dispatchers.Default для CPU-задач.
// Плохо: блокируем UI-поток
fun loadData() = runBlocking {
val data = fetchFromNetwork() // блокирует поток
updateUI(data)
}
// Хорошо: корутины с правильным диспетчером
fun loadDataAsync() = CoroutineScope(Dispatchers.Main).launch {
val data = withContext(Dispatchers.IO) {
fetchFromNetwork()
}
updateUI(data)
}
Избегайте runBlocking в продакшене. Используйте withContext для переключения потоков. Это база оптимизации Kotlin для Android и бэкенда.
4. String: шаблоны и конкатенация
Kotlin-шаблоны строк удобны, но в циклах они создают множество объектов StringBuilder.
// Плохо: в цикле создается много строк
val sb = StringBuilder()
for (i in 1..1000) {
sb.append("Item: $i\n") // каждый раз новая строка
}
// Хорошо: явный StringBuilder
val sb2 = StringBuilder()
for (i in 1..1000) {
sb2.append("Item: ")
sb2.append(i)
sb2.append("\n")
}
Для однократного использования шаблоны ок. В циклах — всегда StringBuilder или buildString { }.
5. lateinit vs lazy: выбирайте осознанно
lateinit не создает делегата, но требует ручной инициализации. lazy потокобезопасен, но добавляет обертку.
// lateinit — без оверхеда, но только var
lateinit var repository: Repository
// lazy — ленивая инициализация с синхронизацией
val config by lazy {
loadConfig()
}
Если свойство инициализируется один раз и не null — lateinit быстрее. Если нужна ленивость и потокобезопасность — lazy.
Типичные ошибки при профилировании Kotlin
Даже с инструментами можно наступить на грабли. Вот три ошибки, которые убивают всю пользу от профилирования Kotlin.
- Профилирование в Debug-режиме — JIT-компилятор не работает, результаты нерепрезентативны. Всегда профилируйте в Release.
- Игнорирование GC — если ваш Kotlin-код создает много мусора (лямбды, автоупаковка), GC будет тормозить. Смотрите на частоту сборок в профилировщике.
- Оптимизация до профилирования — самая частая ошибка. Не "улучшайте" код без замеров. Иногда очевидное решение (inline) дает минус 5%, а не 50%.
Чек-лист: 5 шагов к быстрому Kotlin
Вот краткий