Встроенные функции в Kotlin

Встроенные функции в Котлине

1. обзор

В Kotlin это функцииfirst-class citizens, поэтому мы можем передавать функции или возвращать их, как и другие обычные типы. Тем не менее, представление этих функций во время выполнения иногда может вызвать некоторые ограничения или осложнения производительности.

В этом руководстве мы сначала перечислим две, казалось бы, не связанные проблемы с лямбда-выражениями и дженериками, а затем, после представленияInline Functions, мы увидим, как они могут решить обе эти проблемы, так что приступим!

2. Беда в раю

2.1. Накладные расходы на лямбды в Котлине

Одним из преимуществ функций первоклассных граждан в Котлине является то, что мы можем передать часть поведения другим функциям. Передача функций в виде лямбда-выражений позволяет нам выразить наши намерения более кратко и элегантно, но это только часть истории.

Чтобы исследовать темную сторону лямбда-выражений, давайте изобретем велосипед, объявив функцию расширения для коллекцийfilter:

fun  Collection.filter(predicate: (T) -> Boolean): Collection = // Omitted

Теперь давайте посмотрим, как указанная выше функция компилируется в Java. Сосредоточьтесь на функцииpredicate, которая передается в качестве параметра:

public static final  Collection filter(Collection, kotlin.jvm.functions.Function1);

Обратите внимание, какpredicate обрабатывается с помощью интерфейсаFunction1?

Теперь, если мы назовем это в Kotlin:

sampleCollection.filter { it == 1 }

Будет сделано нечто похожее на следующее, чтобы обернуть лямбда-код:

filter(sampleCollection, new Function1() {
  @Override
  public Boolean invoke(Integer param) {
    return param == 1;
  }
});

Every time we declare a higher-order function, at least one instance of those special Function* types will be created.

Почему Kotlin делает это вместо, скажем,invokedynamic like how Java 8 does with lambdas? Simply put, Kotlin goes for Java 6 compatibility, and invokedynamic isn’t available until Java 7.

Но это еще не конец. Как мы могли догадаться, просто создать экземпляр типа недостаточно.

Чтобы фактически выполнить операцию, инкапсулированную в лямбда-выражении Kotlin, функция высшего порядка - в данном случаеfilter - должна будет вызвать специальный метод с именемinvoke в новом экземпляре. The result is more overhead due to the extra call.

Итак, напомним, когда мы передаем лямбду функции, под капотом происходит следующее:

  1. По крайней мере, один экземпляр специального типа создается и хранится в куче

  2. Дополнительный вызов метода всегда будет происходить

Еще одно выделение экземпляра и еще один вызов виртуального методаdoesn’t seem that bad, right?

2.2. Затворы

Как мы видели ранее, когда мы передаем лямбду функции, будет создан экземпляр типа функции, аналогичныйanonymous inner classes в Java.

Как и в случае с последним, a lambda expression can access its closure, that is, variables declared in the outer scope. . Когда лямбда захватывает переменную из своего закрытия, Kotlin сохраняет переменную вместе с захваченным лямбда-кодом.

The extra memory allocations get even worse when a lambda captures a variable: The JVM creates a function type instance on every invocation. Для лямбда-выражений без захвата будет только один экземпляр,singleton, этих типов функций.

Как мы уверены в этом? Давайте изобретем еще одно колесо, объявив функцию для применения функции к каждому элементу коллекции:

fun  Collection.each(block: (T) -> Unit) {
    for (e in this) block(e)
}

Как бы глупо это ни звучало, здесь мы умножаем каждый элемент коллекции на случайное число:

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val random = random()

    numbers.each { println(random * it) } // capturing the random variable
}

И если мы заглянем внутрь байт-кода, используяjavap:

>> javap -c MainKt
public final class MainKt {
  public static final void main();
    Code:
      // Omitted
      51: new           #29                 // class MainKt$main$1
      54: dup
      55: fload_1
      56: invokespecial #33                 // Method MainKt$main$1."":(F)V
      59: checkcast     #35                 // class kotlin/jvm/functions/Function1
      62: invokestatic  #41                 // Method CollectionsKt.each:(Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)V
      65: return

Затем по индексу 51 мы можем определить, что JVM создает новый экземпляр внутреннего классаMainKt$main$1 для каждого вызова. Кроме того, индекс 56 показывает, как Котлин фиксирует случайную величину. This means that each captured variable will be passed as constructor arguments, thus generating a memory overhead.

2.3. Тип Erasure

Когда дело доходит до дженериков на JVM, это никогда не было раем! В любом случае, Kotlin стирает информацию общего типа во время выполнения. То естьan instance of a generic class doesn’t preserve its type parameters at runtime.

Например, при объявлении нескольких коллекций, таких какList<Int> илиList<String>,, все, что у нас есть во время выполнения, - это простоLists. Как и было обещано, это кажется не связанным с предыдущими проблемами, но мы увидим, как встроенные функции являются общим решением обеих проблем.

3. Встроенные функции

3.1. Снятие накладных лямбд

При использовании лямбды дополнительные выделения памяти и дополнительный вызов виртуального метода вносят некоторые накладные расходы во время выполнения. Итак, если бы мы выполняли тот же код напрямую, а не использовали лямбда-выражения, наша реализация была бы более эффективной.

Придется ли нам выбирать между абстракцией и эффективностью?

As is turns out, with inline functions in Kotlin we can have both! Мы можем написать наши красивые и элегантные лямбдыand the compiler generates the inlined and direct code for us.. Все, что нам нужно сделать, это добавить к немуinline:

inline fun  Collection.each(block: (T) -> Unit) {
    for (e in this) block(e)
}

When using inline functions, the compiler inlines the function body. That is, it substitutes the body directly into places where the function gets called. По умолчанию компилятор встраивает код как для самой функции, так и для переданных ей лямбда-выражений.

Например, компилятор переводит:

val numbers = listOf(1, 2, 3, 4, 5)
numbers.each { println(it) }

Для чего-то вроде:

val numbers = listOf(1, 2, 3, 4, 5)
for (number in numbers)
    println(number)

When using inline functions, there is no extra object allocation and no extra virtual method calls.

Тем не менее, мы не должны чрезмерно использовать встроенные функции, особенно для длинных функций, так как встраивание может привести к тому, что сгенерированный код будет немного расти.

3.2. Нет встроенного

По умолчанию все лямбда-выражения, переданные встроенной функции, также будут встроенными. Однако мы можем пометить некоторые лямбды ключевым словомnoinline, чтобы исключить их из встраивания:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }

3.3. Встроенный Reification

Как мы видели ранее, Kotlin стирает информацию общего типа во время выполнения, но для встроенных функций мы можем избежать этого ограничения. That is, the compiler can reify generic type information for inline functions.с

Все, что нам нужно сделать, это пометить параметр типа skeywordreified :

inline fun  Any.isA(): Boolean = this is T

Безinline иreified функцияisA не скомпилировалась бы, как мы подробно объясняем в нашей статьеKotlin Generics.

4. Ограничения

Как правило,we can inline functions with lambda parameters only if the lambda is either called directly or passed to another inline function.  В противном случае компилятор предотвращает встраивание с ошибкой компилятора.

Например, давайте взглянем на функциюreplace в стандартной библиотеке Kotlin:

inline fun CharSequence.replace(regex: Regex, noinline transform: (MatchResult) -> CharSequence): String =
    regex.replace(this, transform) // passing to a normal function

В приведенном выше фрагменте передается лямбдаtransform, to, обычная функцияreplace, следовательно,noinline.

5. Заключение

В этой статье мы разберемся с проблемами с лямбда-производительностью и стиранием типов в Kotlin. Затем, после введения встроенных функций, мы увидели, как они могут решить обе проблемы.

Однако мы должны стараться не злоупотреблять этими типами функций, особенно когда тело функции слишком велико, поскольку размер сгенерированного байт-кода может возрасти, и мы также можем потерять несколько оптимизаций JVM на этом пути.

Как обычно, образцы кодов доступны наour GitHub project, поэтому обязательно ознакомьтесь с ними!