quando usare una funzione inline in Kotlin?


105

So che una funzione inline forse migliorerà le prestazioni e farà crescere il codice generato, ma non sono sicuro di quando sia corretto usarne una.

lock(l) { foo() }

Invece di creare un oggetto funzione per il parametro e generare una chiamata, il compilatore potrebbe emettere il codice seguente. ( Fonte )

l.lock()
try {
  foo()
}
finally {
  l.unlock()
}

ma ho scoperto che non esiste alcun oggetto funzione creato da kotlin per una funzione non in linea. perché?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}

7
Ci sono due casi d'uso principali per questo, uno è con alcuni tipi di funzioni di ordine superiore e l'altro è parametri di tipo reificato. La documentazione delle funzioni inline copre questi: kotlinlang.org/docs/reference/inline-functions.html
zsmb13

2
@ zsmb13 grazie, signore. ma non lo capisco: "Invece di creare un oggetto funzione per il parametro e generare una chiamata, il compilatore potrebbe emettere il seguente codice"
holi-java

2
non ricevo nemmeno quell'esempio.
filthy_wizard

Risposte:


279

Supponiamo che tu crei una funzione di ordine superiore che accetta un lambda di tipo () -> Unit(nessun parametro, nessun valore di ritorno) e lo esegue in questo modo:

fun nonInlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

Nel gergo Java, questo si tradurrà in qualcosa di simile (semplificato!):

public void nonInlined(Function block) {
    System.out.println("before");
    block.invoke();
    System.out.println("after");
}

E quando lo chiami da Kotlin ...

nonInlined {
    println("do something here")
}

Sotto il cofano, Functionverrà creata un'istanza di qui, che avvolge il codice all'interno di lambda (di nuovo, questo è semplificato):

nonInlined(new Function() {
    @Override
    public void invoke() {
        System.out.println("do something here");
    }
});

Quindi, in pratica, chiamare questa funzione e passarvi un lambda creerà sempre un'istanza di un Functionoggetto.


D'altra parte, se utilizzi la inlineparola chiave:

inline fun inlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

Quando lo chiami in questo modo:

inlined {
    println("do something here")
}

Nessuna Functionistanza verrà creata, invece, il codice attorno all'invocazione di blockall'interno della funzione inline verrà copiato nel sito della chiamata, quindi otterrai qualcosa di simile nel bytecode:

System.out.println("before");
System.out.println("do something here");
System.out.println("after");

In questo caso, non vengono create nuove istanze.


19
Qual è il vantaggio di avere il wrapper dell'oggetto Function in primo luogo? cioè, perché non è tutto in linea?
Arturs Vancans

14
In questo modo puoi anche trasferire arbitrariamente le funzioni come parametri, memorizzarle in variabili, ecc.
zsmb13

6
Ottima spiegazione di @ zsmb13
Yajairo87

2
Puoi, e se fai cose complicate con loro, alla fine vorrai conoscere le parole chiave noinlinee crossinline: vedi i documenti .
zsmb13

2
I documenti forniscono un motivo per cui non si desidera eseguire l'inline per impostazione predefinita: l' inlining può far crescere il codice generato; tuttavia, se lo facciamo in un modo ragionevole (cioè evitando di incorporare grandi funzioni), ripagherà in termini di prestazioni, specialmente nei siti di chiamata "megamorfici" all'interno dei cicli.
CorayThan

43

Vorrei aggiungere: "Quando non usare inline" :

1) Se hai una funzione semplice che non accetta altre funzioni come argomento, non ha senso inserirle in linea. IntelliJ ti avvertirà:

L'impatto previsto sulle prestazioni dell'inlining "..." è insignificante. L'inlining funziona meglio per le funzioni con parametri di tipi funzionali

2) Anche se hai una funzione "con parametri di tipi funzionali", potresti incontrare il compilatore che ti dice che l'inlining non funziona. Considera questo esempio:

inline fun calculateNoInline(param: Int, operation: IntMapper): Int {
    val o = operation //compiler does not like this
    return o(param)
}

Questo codice non verrà compilato con l'errore:

Uso illegale del parametro inline "operation" in "...". Aggiungi il modificatore "noinline" alla dichiarazione del parametro.

Il motivo è che il compilatore non è in grado di incorporare questo codice. Se operationnon è racchiuso in un oggetto (che è implicito da inlinepoiché si desidera evitarlo), come può essere assegnato a una variabile? In questo caso, il compilatore suggerisce di fare l'argomento noinline. Avere una inlinefunzione con una singola noinlinefunzione non ha alcun senso, non farlo. Tuttavia, se sono presenti più parametri di tipi funzionali, valutare la possibilità di incorporarne alcuni se necessario.

Quindi ecco alcune regole suggerite:

  • È possibile inline quando tutti i parametri di tipo funzionale vengono chiamati direttamente o passati a un'altra funzione inline
  • Si dovrebbe Inline quando ^ è il caso.
  • tu possibile inline quando il parametro della funzione viene assegnato a una variabile all'interno della funzione
  • Si dovrebbe prendere in considerazione inlining se almeno uno dei tuoi parametri di tipo funzionale possono essere inline, l'usonoinline per gli altri.
  • Non dovresti incorporare funzioni enormi, pensa al codice byte generato. Verrà copiato in tutte le posizioni da cui viene chiamata la funzione.
  • Un altro caso d'uso sono i reifiedparametri di tipo, che richiedono l'utilizzo di inline. Leggi qui .

4
tecnicamente potresti ancora funzioni inline che non accettano espressioni lambda, giusto? .. qui il vantaggio è che l'overhead della chiamata di funzione è evitato in quel caso .. linguaggio come Scala lo consente .. non sono sicuro del motivo per cui Kotlin proibisce quel tipo di inline- ing
rogue-one

3
@ rogue-one Kotlin non vieta questa volta di inlining. Gli autori del linguaggio stanno semplicemente sostenendo che il vantaggio in termini di prestazioni probabilmente sarà insignificante. È probabile che metodi piccoli vengano già integrati dalla JVM durante l'ottimizzazione JIT, soprattutto se vengono eseguiti frequentemente. Un altro caso in cui inlinepuò essere dannoso è quando il parametro funzionale viene chiamato più volte nella funzione inline, ad esempio in diversi rami condizionali. Mi sono appena imbattuto in un caso in cui tutto il bytecode per gli argomenti funzionali veniva duplicato per questo motivo.
Mike Hill,

5

Il caso più importante quando usiamo il modificatore inline è quando definiamo funzioni tipo util con funzioni parametro. Raccolta o elaborazione di stringhe (come filter, mapo joinToString) o solo funzioni autonome sono un esempio perfetto.

Questo è il motivo per cui il modificatore inline è principalmente un'ottimizzazione importante per gli sviluppatori di librerie. Dovrebbero sapere come funziona e quali sono i suoi miglioramenti e costi. Dovremmo usare il modificatore inline nei nostri progetti quando definiamo le nostre funzioni util con parametri del tipo di funzione.

Se non abbiamo il parametro del tipo di funzione, il parametro del tipo reificato e non abbiamo bisogno di un ritorno non locale, allora molto probabilmente non dovremmo usare il modificatore inline. Questo è il motivo per cui avremo un avviso su Android Studio o IDEA IntelliJ.

Inoltre, c'è un problema di dimensione del codice. L'inlining di una funzione di grandi dimensioni potrebbe aumentare notevolmente la dimensione del bytecode perché viene copiato in ogni sito di chiamata. In questi casi, è possibile effettuare il refactoring della funzione ed estrarre il codice in funzioni regolari.


4

Le funzioni di ordine superiore sono molto utili e possono davvero migliorare il reusabilitycodice. Tuttavia, una delle maggiori preoccupazioni riguardo al loro utilizzo è l'efficienza. Le espressioni Lambda vengono compilate in classi (spesso classi anonime) e la creazione di oggetti in Java è un'operazione pesante. Possiamo ancora utilizzare le funzioni di ordine superiore in modo efficace, mantenendo tutti i vantaggi, rendendo le funzioni in linea.

ecco che arriva la funzione inline nell'immagine

Quando una funzione è contrassegnata come inline, durante la compilazione del codice il compilatore sostituirà tutte le chiamate di funzione con il corpo effettivo della funzione. Inoltre, le espressioni lambda fornite come argomenti vengono sostituite con il loro corpo effettivo. Non verranno trattati come funzioni, ma come codice effettivo.

In breve: - Inline -> invece di essere chiamati, vengono sostituiti dal codice del corpo della funzione in fase di compilazione ...

In Kotlin, l'utilizzo di una funzione come parametro di un'altra funzione (le cosiddette funzioni di ordine superiore) sembra più naturale che in Java.

Tuttavia, l'utilizzo di lambda presenta alcuni svantaggi. Poiché sono classi anonime (e quindi oggetti), hanno bisogno di memoria (e potrebbero anche aumentare il conteggio complessivo dei metodi della tua app). Per evitare ciò, possiamo integrare i nostri metodi.

fun notInlined(getString: () -> String?) = println(getString())

inline fun inlined(getString: () -> String?) = println(getString())

Dall'esempio precedente : - Queste due funzioni fanno esattamente la stessa cosa: stampano il risultato della funzione getString. Uno è inline e uno no.

Se controlli il codice java decompilato, vedrai che i metodi sono completamente identici. Questo perché la parola chiave inline è un'istruzione al compilatore per copiare il codice nel sito di chiamata.

Tuttavia, se stiamo passando qualsiasi tipo di funzione a un'altra funzione come di seguito:

//Compile time error… Illegal usage of inline function type ftOne...
 inline fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
 }

Per risolverlo, possiamo riscrivere la nostra funzione come di seguito:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

Supponiamo di avere una funzione di ordine superiore come di seguito:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

Qui, il compilatore ci dirà di non usare la parola chiave inline quando c'è solo un parametro lambda e lo stiamo passando a un'altra funzione. Quindi, possiamo riscrivere la funzione sopra come di seguito:

fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
}

Nota : -abbiamo dovuto rimuovere anche la parola chiave noinline perché può essere utilizzata solo per le funzioni inline!

Supponiamo di avere una funzione come questa ->

fun intercept() {
    // ...
    val start = SystemClock.elapsedRealtime()
    val result = doSomethingWeWantToMeasure()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    // ...}

Funziona bene, ma la carne della logica della funzione è inquinata dal codice di misurazione, rendendo più difficile per i tuoi colleghi lavorare su quello che sta succedendo. :)

Ecco come una funzione inline può aiutare questo codice:

      fun intercept() {
    // ...
    val result = measure { doSomethingWeWantToMeasure() }
    // ...
    }

     inline fun <T> measure(action: () -> T) {
    val start = SystemClock.elapsedRealtime()
    val result = action()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    return result
    }

Ora posso concentrarmi sulla lettura di quale sia l'intenzione principale della funzione intercept () senza saltare le righe del codice di misurazione. Beneficiamo anche dell'opzione di riutilizzare quel codice in altri posti dove vogliamo

inline ti permette di chiamare una funzione con un argomento lambda all'interno di una chiusura ({...}) invece di passare la misura lambda (myLamda)


2

Un semplice caso in cui potresti volerne uno è quando crei una funzione util che accetta un blocco di sospensione. Considera questo.

fun timer(block: () -> Unit) {
    // stuff
    block()
    //stuff
}

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

    // This is an error
    timer { asyncLogic() }
}

In questo caso, il nostro timer non accetterà le funzioni di sospensione. Per risolverlo, potresti essere tentato di sospenderlo anche tu

suspend fun timer(block: suspend () -> Unit) {
    // stuff
    block()
    // stuff
}

Ma poi può essere utilizzato solo dalle funzioni coroutines / suspend stesse. Quindi finirai per creare una versione asincrona e una versione non asincrona di queste utilità. Il problema scompare se lo rendi in linea.

inline fun timer(block: () -> Unit) {
    // stuff
    block()
    // stuff
}

fun main() {
    // timer can be used from anywhere now
    timer { logic() }

    launch {
        timer { asyncLogic() }
    }
}

Ecco un playground kotlin con lo stato di errore. Rendi il timer in linea per risolverlo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.