Kotlin на максималках: Профилирование и оптимизация производительности

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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

Вот краткий

Блоги

Рекомендаций по книгам