Подтвердить что ты не робот

В чем разница между crossinline и noinline в Котлине?

  • Этот код компилируется с предупреждением (незначительное влияние на производительность):

    inline fun test(noinline f: () -> Unit) {
        thread(block = f)
    }
    
  • Этот код не компилирует (незаконное использование встроенного параметра):

    inline fun test(crossinline f: () -> Unit) {
        thread(block = f)
    }
    
  • Этот код компилируется с предупреждением (незначительное влияние на производительность):

    inline fun test(noinline f: () -> Unit) {
        thread { f() }
    }
    
  • Этот код компилируется без предупреждения или ошибки:

    inline fun test(crossinline f: () -> Unit) {
        thread { f() }
    }
    

Вот мои вопросы:

  • Почему (2) не компилируется, но (4) делает?
  • В чем разница между noinline и crossinline?
  • Если (3) не генерирует никаких улучшений производительности, почему бы (4) сделать?
4b9b3361

Ответ 1

В ссылка встроенных функций:

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

Следовательно, пример 2. не компилируется, так как crossinline обеспечивает только локальный поток управления, а выражение block = f нарушает это. Пример 1 компилируется, поскольку noinline не требует такого поведения (очевидно, так как это обычный параметр функции).

Примеры 1 и 3 не генерируют никаких улучшений производительности, так как единственный параметр лямбда отмечен noinline, рендеринг модификатора inline функции бесполезный и избыточный - компилятор хотел бы встроить что-то, но все, что могло be был отмечен как нестрочный.

Рассмотрим две функции: A и B

А

inline fun test(noinline f: () -> Unit) {
    thread { f() }
}

В

fun test(f: () -> Unit) {
    thread { f() }
}

Функция A ведет себя как функция B в том смысле, что параметр f не будет встроен (функция B не работает вставьте тело test, тогда как в функции A тело: thread { f() } все равно встанет в очередь).

Теперь это неверно в примере 4, так как параметр crossinline f: () -> Unit может быть встроенным, он просто не может нарушить вышеупомянутое правило нелокального управления потоком (например, присвоение нового значения глобальная переменная). И если он может быть встроен, компилятор предполагает повышение производительности и не предупреждает, как в примере 3.

Ответ 2

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

fun main(args: Array<String>) {
    test { 
        println("start")
        println("stop")
    }
}

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

1. noinline, block = f

inline fun test1(noinline f: () -> Unit) {
    thread(block = f)
}

fun compiledMain1() {
    val myBlock = {
        println("start")
        println("stop")
    }
    thread(block = myBlock)
}

Во-первых, обратите внимание, что нет никаких доказательств того, что inline fun test1 даже существует. Встроенные функции на самом деле не "вызываются": это как если бы код test1 был написан внутри main(). С другой стороны, лямбда-параметр noinline ведет себя так же, как и без встраивания: вы создаете лямбда-объект и передаете его в функцию thread.

2. crossinline, block = f

inline fun test2(crossinline f: () -> Unit) {
    thread(block = f)
}

fun compiledMain2() {
    thread(block =
        println("start")
        println("stop")
    )
}

Надеюсь, мне удалось наколдовать, что здесь происходит: вы попросили скопировать и вставить код блока в место, которое ожидает значение. Это просто синтаксический мусор. Причина: с или без crossinline вы запрашиваете, чтобы блок был вставлен в том месте, где он использовался. Этот модификатор просто ограничивает то, что вы можете написать внутри блока (нет return и т.д.)

3. noinline, { f() }

inline fun test3(noinline f: () -> Unit) {
    thread { f() }
}

fun compiledMain3() {
    val myBlock = {
        println("start")
        println("stop")
    }
    thread { myBlock() }
}

Мы вернулись к noinline здесь, так что все снова просто. Вы создаете обычный лямбда-объект myBlock, затем вы создаете еще один обычный лямбда-объект, который ему делегируется: { myBlock() }, а затем передаете его thread().

4. crossinline, { f() }

inline fun test4(crossinline f: () -> Unit) {
    thread { f() }
}

fun compiledMain4() {
    thread {
        println("start")
        println("stop")
    }
}

Наконец, этот пример демонстрирует, для чего crossinline. Код test4 встраивается в main, код блока встраивается в место, где он использовался. Но поскольку он используется внутри определения обычного лямбда-объекта, он не может содержать нелокальный поток управления.

О влиянии на производительность

Команда Kotlin хочет, чтобы вы использовали разумно встроенную функцию. При встраивании размер скомпилированного кода может резко взорваться и даже достигнуть пределов JVM до 64K инструкций байт-кода на метод. Основным вариантом использования являются функции более высокого порядка, которые позволяют избежать затрат на создание реального лямбда-объекта, а только отказаться от него сразу после одного вызова функции, который происходит сразу.

Всякий раз, когда вы объявляете inline fun без встроенных лямбд, встраивание само по себе теряет свою цель. Компилятор предупреждает вас об этом.

Ответ 3

Q1: Почему (2) не скомпилируется, но (4) делает?

Из своего документа:

Inlinable lambdas может быть вызван только внутри встроенных функций или передан как неотъемлемые аргументы...

Ответ:

Метод thread(...) не является inline, поэтому вы не сможете передать f в качестве аргумента.

Q2: В чем именно заключается разница между noinline и crossinline?

Ответ:

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

crossinline используется для отметки lambdas, которая не должна допускать нелокальные возвращения, особенно когда такая lambda передается в другой контекст выполнения. Другими словами, вы не сможете использовать return в таких лямбдах. Используя ваш пример:

inline fun test(crossinline f: () -> Unit) {
    thread { f() }
}

//another method in the class
fun foo() {

    test{ 

       //Error! return is not allowed here.
       return
    }

}

Q3: Если (3) не генерирует никаких улучшений производительности, почему бы (4) сделать?

Ответ:

Это потому, что единственная лямбда, которую вы имеете в (3), была отмечена noinline, что означает, что у вас будут накладные расходы на создание объекта Function для размещения тела вашей лампы. Для (4) лямбда по-прежнему встроена (повышение производительности) только для того, чтобы она не позволяла нелокальные возвращения.

Ответ 4

К первому и второму вопросу

Почему (2) не компилируется, но (4) делает?.. разница между noinline и crossinline

2. inline fun test(crossinline f: () -> Unit) {
    thread(block = f)
}

4. inline fun test(crossinline f: () -> Unit) {
    thread { f() }
}

Оба случая имеют модификатор inline, инструктирующий встраивать как функцию test, так и ее аргумент lambda f. Из ссылки kotlin:

Встроенный модификатор влияет как на саму функцию, так и на lambdas передается ему: все они будут включены в сайт вызова.

Поэтому компилятору предлагается поместить код (inline) вместо создания и вызова объекта функции для f. Модификатор crossinline предназначен только для встроенных вещей: он просто говорит, что переданный лямбда (в параметре f) не должен иметь нелокальных возвратов (которые могут иметь нормальные вложенные lambdas). crossinline можно считать чем-то вроде этого (инструкция для компилятора): "сделайте inline, но есть ограничение, что оно пересекает контекст invoker и поэтому убедитесь, что лямбда не имеет нелокальных возвратов.

На стороне примечания thread представляется концептуально иллюстративным примером для crossinline, потому что, очевидно, возврат из некоторого кода (переданного в f) позже в другом потоке не может повлиять на возврат из test, который продолжает выполняться в потоке вызывающего абонента независимо от того, что он породил (f продолжает выполняться независимо).

В случае №4 есть лямбда (фигурные скобки), вызывающие f(). В случае, если # 2, f передается непосредственно в качестве аргумента thread

Итак, в # 4 вызов f() может быть встроен, и компилятор может гарантировать отсутствие нелокального возврата. Чтобы уточнить, компилятор заменил бы f() своим определением и этот код затем "обернут" внутри охватывающей лямбда, другими словами, { //code for f() } является своего рода другой (оберткой) лямбдой, и сам он далее передается как функция ссылка объекта (на thread).

В случае №2 ошибка компилятора просто говорит, что он не может встроить f, потому что он передается в качестве ссылки в "неизвестное" (не вложенное) место. crossinline становится неуместным и неуместным в этом случае, потому что он может применяться только в том случае, если f были вложены.

Подводя итог, случаи 2 и 4 не совпадают по сравнению с примером из ссылки kotlin (см. "Функции более высокого порядка и Lambdas" ): ниже вызывают эквивалентные выражения, в которых фигурные скобки (лямбда-выражение) заменяют "оберточная функция toBeSynchronized

//want to pass `sharedResource.operation()` to lock body
fun <T> lock(lock: Lock, body: () -> T): T {...}
//pass a function
fun toBeSynchronized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchronized) 
//or pass a lambda expression
val result = lock(lock, { sharedResource.operation() })

Случай №2 и №4 в вопросе не эквивалентны, потому что нет "обертки", вызывающей f в # 2