19 KiB
title, description, tags
| title | description | tags | |||
|---|---|---|---|---|---|
| Inline-функции | Подстановка тела на этапе компиляции, noinline и crossinline, нелокальный return и цена inline |
|
inline говорит компилятору: не вызывай эту функцию, а подставь её тело в место вызова. Для обычных функций это почти бесполезно (JIT справляется сам), а для функций с лямбдами - убирает объект лямбды.
inline fun measure(block: () -> Unit): Long {
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
fun main() {
val time = measure { (1..1000).sum() }
println(time > 0) // true
}
После компиляции в main не остаётся ни вызова measure, ни объекта Function0 - там просто val start = ...; (1..1000).sum(); ....
Что именно экономится
Без inline на каждый вызов создаётся объект, реализующий Function0:
fun notInlined(block: () -> Int): Int = block()
inline fun inlined(block: () -> Int): Int = block()
fun main() {
var captured = 0
repeat(3) { notInlined { captured + 1 } } // лямбда захватывает captured -> новый объект каждый раз
repeat(3) { inlined { captured + 1 } } // объектов нет вообще
println(notInlined { 42 }) // 42
println(inlined { 42 }) // 42
}
Лямбда без захвата компилируется в синглтон, поэтому объект создаётся один раз - тут inline экономит только вызов. Лямбда с захватом создаёт объект на каждое вычисление, и вот здесь разница существенна.
Поэтому все операции над коллекциями в stdlib объявлены inline:
fun main() {
val sum = listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 2 }
println(sum) // 10
}
В байт-коде это два обычных цикла for с телами лямбд внутри. Ни одного объекта лямбды и ни одного виртуального вызова - именно поэтому короткие цепочки в Kotlin обычно быстрее Java-стримов.
Нелокальный return
Второе следствие подстановки: из inline-лямбды можно выйти из внешней функции.
inline fun <T> List<T>.forEachInline(action: (T) -> Unit) { for (item in this) action(item) }
fun findFirstNegative(numbers: List<Int>): Int? {
numbers.forEachInline { if (it < 0) return it } // выход из findFirstNegative
return null
}
fun main() {
println(findFirstNegative(listOf(1, 2, -3, 4))) // -3
println(findFirstNegative(listOf(1, 2))) // null
}
Это работает, потому что return оказывается физически внутри findFirstNegative. В не-inline функции такой return запрещён:
fun <T> List<T>.forEachSlow(action: (T) -> Unit) { for (item in this) action(item) }
fun firstNegative2(numbers: List<Int>): Int? {
numbers.forEachSlow { if (it < 0) return@forEachSlow } // только локальный выход
return null
}
fun main() { println(firstNegative2(listOf(-1))) } // null
Именно поэтому forEach из stdlib ведёт себя как continue, а не как break:
fun main() {
listOf(1, 2, 3).forEach {
if (it == 2) return@forEach // пропустить элемент
print("$it ")
}
println() // 1 3
}
noinline
Если лямбду нужно сохранить или передать дальше, подставить её нельзя - у подставленного тела нет объекта:
inline fun register(
name: String,
noinline handler: (String) -> Unit,
validator: (String) -> Boolean,
): (String) -> Unit {
println("регистрирую $name, валиден: ${validator(name)}")
return handler // возвращаем объект — нужен noinline
}
fun main() {
val h = register("событие", handler = { println("обработано: $it") }, validator = { it.isNotEmpty() })
h("данные")
// регистрирую событие, валиден: true
// обработано: данные
}
Без noinline компилятор откажется: «Illegal usage of inline-parameter». Правило: лямбду, которая только вызывается внутри функции, можно инлайнить; лямбду, которую сохраняют в поле, возвращают или передают в не-inline функцию - нельзя.
crossinline
Промежуточный случай: лямбда вызывается не напрямую, а внутри другой лямбды или объекта.
inline fun runLater(crossinline block: () -> Unit): Runnable = Runnable { block() }
fun main() {
val task = runLater { println("выполнено позже") }
task.run() // выполнено позже
}
crossinline означает «инлайнить можно, но нелокальный return запрещён». Логика такая: тело лямбды подставляется внутрь Runnable, а оттуда выйти из вызывающей функции физически невозможно - она уже завершилась.
inline fun repeatTwice(crossinline block: () -> Unit) {
val wrapped = { block(); block() }
wrapped()
}
fun demo() {
repeatTwice { println("раз") }
// repeatTwice { return } // ошибка: нелокальный return запрещён
}
fun main() { demo() } // раз \n раз
| Модификатор | Лямбда подставляется | Нелокальный return |
Можно сохранить/передать |
|---|---|---|---|
| (по умолчанию) | да | да | нет |
crossinline |
да | нет | только внутрь другой лямбды |
noinline |
нет | нет | да |
reified
Третье следствие подстановки: в месте вызова известен фактический тип, поэтому параметр типа можно «материализовать».
inline fun <reified T> filterByType(items: List<Any>): List<T> = items.filterIsInstance<T>()
fun main() {
val mixed = listOf(1, "a", 2.0, 3)
println(filterByType<Int>(mixed)) // [1, 3]
println(filterByType<String>(mixed)) // [a]
}
Это основная причина объявлять inline функцию без лямбд вообще. Подробнее: Reified и inline
Inline-свойства
inline применим и к аксессорам:
class Config2(val map: Map<String, String>) {
inline val isEmpty: Boolean get() = map.isEmpty()
val host: String
inline get() = map["host"] ?: "localhost"
}
fun main() {
val c = Config2(mapOf("host" to "example.com"))
println("${c.isEmpty} ${c.host}") // false example.com
}
Обратите внимание: map объявлен val без private. Для private val сниппет не скомпилируется - на inline-аксессоры действует то же правило, что на inline-функции: подставленное тело окажется в чужом классе, где приватный член недоступен.
Выгода минимальна - JIT инлайнит такие геттеры и сам. Реально inline-свойства нужны редко: когда геттер должен использовать reified или когда свойство - часть публичного API, которое не хочется оставлять виртуальным.
Когда inline оправдан
| Случай | Нужен ли inline |
|---|---|
| функция принимает лямбду, которая вызывается сразу | да |
нужен reified |
да, обязательно |
нужен нелокальный return из лямбды |
да, обязательно |
функция без лямбд и без reified |
нет, JIT справится |
| тело большое (десятки строк) | нет, байт-код раздуется |
| функция рекурсивная | нельзя |
| публичное API библиотеки | осторожно - см. подводные камни |
Что происходит с байт-кодом
inline fun twice(block: () -> Unit) { block(); block() }
fun main() {
var count = 0
twice { count++ }
twice { count += 10 }
println(count) // 22
}
Тело twice скопировано в main дважды, и каждая копия содержит своё тело лямбды - итого четыре инкремента в коде. Для короткой функции это дешевле вызова; для функции на сто строк, вызываемой из пятидесяти мест, это пять тысяч строк байт-кода.
Компилятор предупреждает, когда inline бесполезен:
inline fun uselessInline(x: Int): Int = x * 2 // warning: expected performance impact is insignificant
fun main() { println(uselessInline(2)) } // 4
Такое предупреждение - сигнал убрать inline, если не нужен reified.
[!info] Отличия от Java
Java Kotlin нет inline на уровне языка inline funJIT инлайнит в рантайме компилятор инлайнит в байт-код лямбда всегда объект при inlineобъекта нетreturnиз лямбды невозможеннелокальный returnприinlineClass<T>параметромreified Tfinalподсказывает JITinline- директива компиляторуJava инлайнит в рантайме, Kotlin - при компиляции. HotSpot инлайнит горячие методы агрессивнее, чем любой компилятор: он знает фактические типы, статистику ветвлений и может деоптимизировать решение. Поэтому
inlineв Kotlin - не про скорость исполнения тела, а про три вещи, недоступные JIT: отсутствие объекта лямбды, нелокальныйreturnиreified.Escape analysis частично закрывает ту же проблему в Java. Если JIT доказывает, что объект лямбды не покидает метод, он может не выделять его в куче (scalar replacement). Но это оптимизация «на удачу»: она применяется не всегда и ломается при полиморфном вызове.
inlineв Kotlin гарантирован и виден в байт-коде.Нелокального
returnв Java нет вообще. Из телаforEachнельзя выйти из внешнего метода - приходится либо возвращаться к обычномуfor, либо бросать исключение, либо использоватьfindFirstна стриме. Kotlin позволяет писать абстракции обхода, которые ведут себя как встроенный цикл:inline fun <T> Iterable<T>.firstMatching(p: (T) -> Boolean): T? { for (item in this) if (p(item)) return item return null } fun main() { println(listOf(1, 2, 3).firstMatching { it > 1 }) } // 2Цена - совместимость библиотек. В Java изменение тела метода не требует перекомпиляции клиентов. В Kotlin тело inline-функции физически скопировано в байт-код клиента, поэтому исправление в библиотеке не подействует до пересборки вызывающего кода. Для публичного API это серьёзный аргумент, и stdlib помечает такие функции
@PublishedApi, когда они обращаются к internal-коду.Inline-функцию нельзя вызвать из Java с сохранением смысла: Java-компилятор не умеет подставлять тело, он вызовет сгенерированный метод как обычный.
reifiedпри этом не работает.Сравнить: JIT в Java, Байт-код
[!warning] Подводные камни Inline раздувает байт-код. Тело копируется в каждое место вызова. Для больших функций это рост размера класса, давление на кеш инструкций процессора и более медленная загрузка классов.
inlineоправдан для коротких функций - в stdlib почти все inline-функции занимают несколько строк.Изменение тела inline-функции не действует без перекомпиляции клиентов:
inline fun version() = "v1" // если выпустить v2, клиенты со старой сборкой увидят v1 fun main() { println(version()) } // v1Это ломает обычные ожидания от семантического версионирования библиотек.
Inline-функция не видит
privateчленов своего класса:class Service3 { private val secret = "скрыто" // inline fun leak() = secret // ошибка: public-inline не может обращаться к private fun ok() = secret internal inline fun fine() = ok() // а так можно } fun main() { println(Service3().fine()) } // скрытоПричина: тело окажется в чужом классе, где
privateнедоступен. Обход -@PublishedApi internalна члене.Рекурсивную функцию нельзя сделать inline:
// inline fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) // ошибка fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) fun main() { println(fact(5)) } // 120Подстановка была бы бесконечной.
Лямбду из inline-параметра нельзя сохранить:
inline fun broken(block: () -> Unit) { // val saved = block // ошибка: Illegal usage of inline-parameter block() } inline fun fixed(noinline block: () -> Unit): () -> Unit = block fun main() { broken { println("ок") } fixed { println("сохранено") }() }
crossinlineзапрещает нелокальныйreturn, и ошибка возникает у пользователя функции, а не у автора. Если лямбда оборачивается вRunnable, объект или другую лямбду -crossinlineобязателен, и вызывающий код теряет возможность выйти из своей функции.Нелокальный
returnиз inline-лямбды пропускает код после вызова:inline fun withLogging(block: () -> Unit) { println("до") block() println("после") // не выполнится при return из лямбды } fun run1() { withLogging { return } } fun main() { run1(); println("конец") } // до \n конецСтрока «после» потерялась. Если в функции есть освобождение ресурсов, его нужно ставить в
finally- иначе нелокальныйreturnего обойдёт:inline fun safeLogging(block: () -> Unit) { println("до") try { block() } finally { println("после") } } fun run2() { safeLogging { return } } fun main() { run2() } // до \n послеЭто важнее, чем кажется:
use,withLockи транзакционные обёртки обязаны использоватьfinallyименно по этой причине.
inlineна функции без лямбд вызывает предупреждение и почти не влияет на производительность. Единственные оправдания -reifiedи необходимость подставить тело в клиентский код.Внутри inline-лямбды нельзя использовать
suspend-вызовы, если не помечено специально. Для функций вродеmeasureэто выглядит рабочим, но обёртка надsuspend-кодом требует либоsuspend inline, либоcrossinlineи явного учёта контекста. Подробнее: Suspend-функцииИнлайн не убирает упаковку примитивов в функциональных типах автоматически - но при полной подстановке упаковки не остаётся, потому что лямбды как объекта нет. А вот с
noinlineпараметром(Int) -> Intупаковка сохраняется.Профилируйте, прежде чем добавлять
inlineвручную. Накладные расходы лямбды измеримы только в горячем коде с миллионами вызовов. В обычном бизнес-коде преждевременныйinlineдаёт лишь раздутый байт-код и проблемы совместимости.
Далее: Функции-расширения