Wann soll eine Inline-Funktion in Kotlin verwendet werden?

104

Ich weiß, dass eine Inline-Funktion möglicherweise die Leistung verbessert und den generierten Code wachsen lässt, aber ich bin mir nicht sicher, wann es richtig ist, eine zu verwenden.

lock(l) { foo() }

Anstatt ein Funktionsobjekt für den Parameter zu erstellen und einen Aufruf zu generieren, könnte der Compiler den folgenden Code ausgeben. ( Quelle )

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

Ich habe jedoch festgestellt, dass für eine Nicht-Inline-Funktion kein von kotlin erstelltes Funktionsobjekt vorhanden ist. Warum?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}
Holi-Java
quelle
7
Hierfür gibt es zwei Hauptanwendungsfälle: Der eine betrifft bestimmte Arten von Funktionen höherer Ordnung und der andere sind reifizierte Typparameter. Die Dokumentation der Inline-Funktionen behandelt diese: kotlinlang.org/docs/reference/inline-functions.html
zsmb13
2
@ zsmb13 danke, sir. aber ich verstehe das nicht: "Anstatt ein Funktionsobjekt für den Parameter zu erstellen und einen Aufruf zu generieren, könnte der Compiler den folgenden Code ausgeben"
holi-java
2
Ich verstehe dieses Beispiel auch nicht.
filthy_wizard

Antworten:

276

Angenommen, Sie erstellen eine Funktion höherer Ordnung, die ein Lambda vom Typ () -> Unit(keine Parameter, kein Rückgabewert) verwendet und wie folgt ausführt:

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

Im Java-Sprachgebrauch bedeutet dies Folgendes (vereinfacht!):

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

Und wenn Sie es aus Kotlin nennen ...

nonInlined {
    println("do something here")
}

Unter der Haube wird hier eine Instanz von Functionerstellt, die den Code in das Lambda einschließt (dies ist wiederum vereinfacht):

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

Wenn Sie diese Funktion aufrufen und ein Lambda an sie übergeben, wird im Grunde immer eine Instanz eines FunctionObjekts erstellt.


Auf der anderen Seite, wenn Sie das inlineSchlüsselwort verwenden:

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

Wenn Sie es so nennen:

inlined {
    println("do something here")
}

Es Functionwird keine Instanz erstellt, stattdessen wird der Code um den Aufruf blockinnerhalb der Inline-Funktion auf die Aufrufsite kopiert, sodass Sie im Bytecode Folgendes erhalten:

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

In diesem Fall werden keine neuen Instanzen erstellt.

zsmb13
quelle
19
Was ist der Vorteil, wenn der Function Object Wrapper überhaupt vorhanden ist? dh - warum ist nicht alles inline?
Arturs Vancans
14
Auf diese Weise können Sie Funktionen auch willkürlich als Parameter übergeben, in Variablen speichern usw.
zsmb13
6
Tolle Erklärung von @ zsmb13
Yajairo87
2
Sie können, und wenn Sie komplizierte Dinge mit ihnen machen, möchten Sie schließlich etwas über die noinlineund crossinlineSchlüsselwörter wissen - siehe die Dokumente .
zsmb13
2
Die Dokumente geben einen Grund an, den Sie nicht standardmäßig inline möchten: Inlining kann dazu führen, dass der generierte Code wächst. Wenn wir dies jedoch auf vernünftige Weise tun (dh das Inlining großer Funktionen vermeiden), zahlt sich dies in der Leistung aus, insbesondere an "megamorphen" Aufrufstellen innerhalb von Schleifen.
CorayThan
43

Lassen Sie mich hinzufügen: "Wann nicht zu verwenden inline" :

1) Wenn Sie eine einfache Funktion haben, die andere Funktionen nicht als Argument akzeptiert, ist es nicht sinnvoll, sie zu integrieren. IntelliJ warnt Sie:

Die erwarteten Auswirkungen des Inlining '...' auf die Leistung sind unbedeutend. Inlining funktioniert am besten für Funktionen mit Parametern von Funktionstypen

2) Selbst wenn Sie eine Funktion "mit Parametern von Funktionstypen" haben, kann der Compiler Ihnen mitteilen, dass Inlining nicht funktioniert. Betrachten Sie dieses Beispiel:

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

Dieser Code wird mit dem Fehler nicht kompiliert:

Unzulässige Verwendung des Inline-Parameters 'operation' in '...'. Fügen Sie der Parameterdeklaration den Modifikator 'noinline' hinzu.

Der Grund ist, dass der Compiler diesen Code nicht einbinden kann. Wenn operationes nicht in ein Objekt eingeschlossen ist (was impliziert wird, inlineda Sie dies vermeiden möchten), wie kann es überhaupt einer Variablen zugewiesen werden? In diesem Fall schlägt der Compiler vor, das Argument vorzubringen noinline. Eine inlineFunktion mit einer einzigen noinlineFunktion zu haben, macht keinen Sinn, tun Sie das nicht. Wenn jedoch mehrere Parameter von Funktionstypen vorhanden sind, sollten Sie bei Bedarf einige davon einfügen.

Hier sind einige vorgeschlagene Regeln:

  • Du können inline, wenn alle Funktionstypparameter direkt aufgerufen oder an eine andere Inline- Funktion übergeben werden
  • Sie sollten inline, wenn ^ der Fall ist.
  • Du können nicht inline arbeiten, wenn Funktionsparameter einer Variablen innerhalb der Funktion zugewiesen werden
  • Du solltest Inlining in Betracht ziehen, wenn mindestens einer Ihrer Funktionstypparameter inliniert werden kann. Verwenden Sie ihn noinlinefür die anderen.
  • Du sollten keine großen Funktionen einbinden, sondern über generierten Bytecode nachdenken. Es wird an alle Stellen kopiert, von denen aus die Funktion aufgerufen wird.
  • Ein weiterer Anwendungsfall sind reifiedTypparameter, die Sie verwenden müssen inline. Lesen Sie hier .
s1m0nw1
quelle
4
Technisch gesehen könnten Sie immer noch Inline-Funktionen verwenden, die keine Lambda-Ausdrücke verwenden, oder? .. hier besteht der Vorteil darin, dass der Funktionsaufruf-Overhead in diesem Fall vermieden wird .. Sprache wie Scala erlaubt dies .. nicht sicher, warum Kotlin diese Art von Inline- verbietet- ing
Rogue-One
3
@ Rogue-One Kotlin verbietet diese Zeit des Inlining nicht. Die Sprachautoren behaupten lediglich, dass der Leistungsvorteil wahrscheinlich unbedeutend ist. Kleine Methoden werden wahrscheinlich bereits während der JIT-Optimierung von der JVM eingebunden, insbesondere wenn sie häufig ausgeführt werden. Ein weiterer Fall inline, der schädlich sein kann, besteht darin, dass der Funktionsparameter in der Inline-Funktion mehrmals aufgerufen wird, z. B. in verschiedenen bedingten Zweigen. Ich bin gerade auf einen Fall gestoßen, in dem der gesamte Bytecode für funktionale Argumente aus diesem Grund dupliziert wurde.
Mike Hill
5

Der wichtigste Fall bei Verwendung des Inline-Modifikators ist die Definition von util-ähnlichen Funktionen mit Parameterfunktionen. Sammlung oder String - Verarbeitung (wie filter, mapoder joinToString) oder Standalone - Funktionen sind ein perfektes Beispiel.

Aus diesem Grund ist der Inline-Modifikator hauptsächlich eine wichtige Optimierung für Bibliotheksentwickler. Sie sollten wissen, wie es funktioniert und welche Verbesserungen und Kosten es hat. Wir sollten den Inline-Modifikator in unseren Projekten verwenden, wenn wir unsere eigenen util-Funktionen mit Funktionstypparametern definieren.

Wenn wir keinen Funktionstypparameter und keinen reifizierten Typparameter haben und keine nicht lokale Rückgabe benötigen, sollten wir den Inline-Modifikator höchstwahrscheinlich nicht verwenden. Aus diesem Grund erhalten wir eine Warnung für Android Studio oder IDEA IntelliJ.

Es gibt auch ein Problem mit der Codegröße. Das Inlinen einer großen Funktion kann die Größe des Bytecodes erheblich erhöhen, da er auf jede Anrufstelle kopiert wird. In solchen Fällen können Sie die Funktion umgestalten und Code in reguläre Funktionen extrahieren.

0xAliHn
quelle
4

Funktionen höherer Ordnung sind sehr hilfreich und können den reusabilityCode wirklich verbessern . Eines der größten Probleme bei der Verwendung ist jedoch die Effizienz. Lambda-Ausdrücke werden zu Klassen kompiliert (häufig anonyme Klassen), und die Objekterstellung in Java ist eine schwere Operation. Wir können weiterhin Funktionen höherer Ordnung effektiv nutzen und dabei alle Vorteile beibehalten, indem wir Funktionen inline machen.

Hier kommt die Inline-Funktion ins Bild

Wenn eine Funktion als markiert ist inline, ersetzt der Compiler während der Codekompilierung alle Funktionsaufrufe durch den eigentlichen Funktionskörper. Außerdem werden als Argumente bereitgestellte Lambda-Ausdrücke durch ihren tatsächlichen Körper ersetzt. Sie werden nicht als Funktionen behandelt, sondern als tatsächlicher Code.

Kurz gesagt: - Inline -> anstatt aufgerufen zu werden, werden sie beim Kompilieren durch den Body-Code der Funktion ersetzt ...

In Kotlin fühlt sich die Verwendung einer Funktion als Parameter einer anderen Funktion (sogenannte Funktionen höherer Ordnung) natürlicher an als in Java.

Die Verwendung von Lambdas hat jedoch einige Nachteile. Da es sich um anonyme Klassen (und damit um Objekte) handelt, benötigen sie Speicher (und können sogar die Gesamtanzahl der Methoden Ihrer App erhöhen). Um dies zu vermeiden, können wir unsere Methoden einbinden.

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

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

Aus dem obigen Beispiel : - Diese beiden Funktionen machen genau dasselbe - Drucken des Ergebnisses der Funktion getString. Einer ist inline und einer nicht.

Wenn Sie den dekompilierten Java-Code überprüfen würden, würden Sie feststellen, dass die Methoden vollständig identisch sind. Dies liegt daran, dass das Inline-Schlüsselwort eine Anweisung an den Compiler ist, den Code in die Aufrufseite zu kopieren.

Wenn wir jedoch einen Funktionstyp wie folgt an eine andere Funktion übergeben:

//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)
    /*...*/
 }

Um dies zu lösen, können wir unsere Funktion wie folgt umschreiben:

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)
    /*...*/}

Angenommen, wir haben eine Funktion höherer Ordnung wie folgt:

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

Hier weist uns der Compiler an, das Inline-Schlüsselwort nicht zu verwenden, wenn nur ein Lambda-Parameter vorhanden ist und wir es an eine andere Funktion übergeben. Wir können also die obige Funktion wie folgt umschreiben:

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

Hinweis : - Wir mussten auch das Schlüsselwort noinline entfernen, da es nur für Inline-Funktionen verwendet werden kann!

Angenommen, wir haben eine Funktion wie diese ->

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

Dies funktioniert gut, aber das Fleisch der Funktionslogik ist mit Messcode verschmutzt, was es Ihren Kollegen erschwert, an den Vorgängen zu arbeiten. :) :)

So kann eine Inline-Funktion diesem Code helfen:

      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
    }

Jetzt kann ich mich darauf konzentrieren, die Hauptabsicht der Funktion intercept () zu lesen, ohne die Zeilen des Messcodes zu überspringen. Wir profitieren auch von der Möglichkeit, diesen Code an anderen Stellen wiederzuverwenden, an denen wir möchten

Inline können Sie eine Funktion mit einem Lambda-Argument innerhalb eines Abschlusses ({...}) aufrufen, anstatt das Lambda-ähnliche Maß (myLamda) zu übergeben.

Wini
quelle
2

Ein einfacher Fall, in dem Sie möglicherweise eine wünschen, ist das Erstellen einer util-Funktion, die einen Suspend-Block enthält. Bedenken Sie.

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

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

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

In diesem Fall akzeptiert unser Timer keine Suspend-Funktionen. Um es zu lösen, könnten Sie versucht sein, es ebenfalls auszusetzen

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

Aber dann kann es nur von Coroutinen / Suspend-Funktionen selbst verwendet werden. Am Ende erstellen Sie eine asynchrone und eine nicht asynchrone Version dieser Dienstprogramme. Das Problem verschwindet, wenn Sie es inline machen.

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

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

    launch {
        timer { asyncLogic() }
    }
}

Hier ist ein Kotlin-Spielplatz mit dem Fehlerzustand. Machen Sie den Timer inline, um es zu lösen.

Anthony Naddeo
quelle