Inline-functies in Kotlin

1. Overzicht

In Kotlin zijn functies eersteklas burgers, dus we kunnen functies doorgeven of teruggeven, net als andere normale typen. De weergave van deze functies tijdens runtime kan echter soms enkele beperkingen of prestatiecomplicaties veroorzaken.

In deze tutorial gaan we eerst twee schijnbaar niet-gerelateerde problemen over lambda's en generieke geneesmiddelen opsommen en daarna, na de introductie van Inline-functies, we zullen zien hoe ze beide zorgen kunnen aanpakken, dus laten we aan de slag gaan!

2. Problemen in het paradijs

2.1. De overhead van Lambdas in Kotlin

Een van de voordelen van functies als eersteklasburgers in Kotlin, is dat we een stukje gedrag kunnen doorgeven aan andere functies. Door functies door te geven als lambdas, kunnen we onze bedoelingen beknopter en eleganter uitdrukken, maar dat is slechts een deel van het verhaal.

Laten we, om de donkere kant van lambda's te verkennen, het wiel opnieuw uitvinden door een uitbreidingsfunctie te declareren filter collecties:

fun Collection.filter (predikaat: (T) -> Boolean): Collection = // Weggelaten

Laten we nu eens kijken hoe de bovenstaande functie in Java compileert. Focus op de predikaat functie die wordt doorgegeven als parameter:

openbare statische eindcollectiefilter (Collection, kotlin.jvm.functions.Function1);

Merk op hoe de predikaat wordt afgehandeld met behulp van de Functie 1 koppel?

Als we dit nu in Kotlin noemen:

sampleCollection.filter {it == 1}

Iets soortgelijks als het volgende zal worden geproduceerd om de lambda-code in te pakken:

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

Elke keer dat we een functie van hogere orde declareren, ten minste één exemplaar van die speciale Functie* typen worden gemaakt.

Waarom doet Kotlin dit in plaats van bijvoorbeeld invokedynamic zoals hoe Java 8 doet met lambda's? Simpel gezegd, Kotlin gaat voor Java 6-compatibiliteit, en invokedynamic is niet beschikbaar tot Java 7.

Maar dit is niet het einde. Zoals we misschien raden, is het niet voldoende om alleen een instantie van een type te maken.

Om de bewerking die is ingekapseld in een Kotlin-lambda daadwerkelijk uit te voeren, moet de functie van hogere orde - filter in dit geval - zal de speciale methode met de naam moeten aanroepen beroep doen op op de nieuwe instantie. Het resultaat is meer overhead door het extra telefoontje.

Dus, om samen te vatten, wanneer we een lambda aan een functie doorgeven, gebeurt het volgende onder de motorkap:

  1. Er wordt ten minste één exemplaar van een speciaal type gemaakt en in de heap opgeslagen
  2. Er zal altijd een extra methodeaanroep plaatsvinden

Nog een instantietoewijzing en nog een virtuele methodeaanroep lijkt niet zo slecht, toch?

2.2. Sluitingen

Zoals we eerder zagen, wanneer we een lambda doorgeven aan een functie, wordt een instantie van een functietype gemaakt, vergelijkbaar met anonieme innerlijke klassen in Java.

Net als bij de laatste, een lambda-expressie heeft toegang tot zijn sluiting, dat wil zeggen variabelen die zijn gedeclareerd in de buitenste scope. Wanneer een lambda een variabele van zijn sluiting vastlegt, slaat Kotlin de variabele op samen met de vastleggende lambda-code.

De extra geheugentoewijzingen worden nog erger wanneer een lambda een variabele vangt: De JVM maakt bij elke aanroep een instantie van een functietype. Voor lambda's die niet worden vastgelegd, is er slechts één instantie, een singleton, van die functietypen.

Hoe weten we dat zo zeker? Laten we een ander wiel opnieuw uitvinden door een functie te declareren om een ​​functie op elk verzamelingselement toe te passen:

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

Hoe gek het ook mag klinken, hier gaan we elk verzamelingselement vermenigvuldigen met een willekeurig getal:

fun main () {val numbers = listOf (1, 2, 3, 4, 5) val random = random () numbers.each {println (random * it)} // capture the random variable}

En als we een kijkje nemen in de bytecode met javap:

>> javap -c MainKt openbare laatste klasse MainKt {openbare statische laatste leegte main (); Code: // Weggelaten 51: nieuw # 29 // klasse MainKt $ main $ 1 54: dup 55: fload_1 56: invokespecial # 33 // Methode 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

Dan kunnen we uit index 51 zien dat de JVM een nieuw exemplaar van MainKt $ main $1 innerlijke klasse voor elke aanroep. Index 56 laat ook zien hoe Kotlin de willekeurige variabele vangt. Dit betekent dat elke vastgelegde variabele wordt doorgegeven als constructorargumenten, waardoor geheugenoverhead wordt gegenereerd.

2.3. Typ Wissen

Als het gaat om generieke geneesmiddelen op de JVM, is het om te beginnen nooit een paradijs geweest! Hoe dan ook, Kotlin wist de algemene type-informatie tijdens runtime. Dat is, een instantie van een generieke klasse behoudt zijn typeparameters niet tijdens runtime.

Bijvoorbeeld bij het aangeven van een aantal verzamelingen zoals Lijst of Lijst, alles wat we tijdens runtime hebben, is gewoon rauw Lijsts. Dit lijkt, zoals beloofd, geen verband te houden met de vorige problemen, maar we zullen zien hoe inline-functies de algemene oplossing zijn voor beide problemen.

3. Inline functies

3.1. De overhead van Lambdas verwijderen

Bij het gebruik van lambdas, introduceren de extra geheugentoewijzingen en extra virtuele methodeaanroep enige runtime-overhead. Dus als we dezelfde code rechtstreeks zouden uitvoeren, in plaats van lambda's te gebruiken, zou onze implementatie efficiënter zijn.

Moeten we kiezen tussen abstractie en efficiëntie?

Zoals blijkt, met inline-functies in Kotlin kunnen we beide hebben! We kunnen onze mooie en elegante lambda's schrijven, en de compiler genereert de inline en directe code voor ons. Het enige wat we hoeven te doen is een in lijn ben ermee bezig:

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

Bij het gebruik van inline-functies, de compiler inline de functie body. Dat wil zeggen, het vervangt het lichaam rechtstreeks naar plaatsen waar de functie wordt aangeroepen. Standaard plaatst de compiler de code voor zowel de functie zelf als de lambda's die eraan worden doorgegeven.

De compiler vertaalt bijvoorbeeld:

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

Op zoiets als:

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

Bij het gebruik van inline-functies is er geen extra objecttoewijzing en geen extra virtuele methodeaanroepen.

We moeten de inline-functies echter niet te veel gebruiken, vooral niet voor lange functies, aangezien de inlining ervoor kan zorgen dat de gegenereerde code behoorlijk groeit.

3.2. Geen inline

Standaard worden alle lambda's die aan een inline-functie worden doorgegeven, ook inline weergegeven. We kunnen echter enkele lambda's markeren met de noinline trefwoord om ze uit te sluiten van inlining:

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

3.3. Inline reificatie

Zoals we eerder zagen, wist Kotlin de algemene type-informatie tijdens runtime, maar voor inline-functies kunnen we deze beperking vermijden. Dat wil zeggen, de compiler kan generieke typegegevens voor inline-functies aanpassen.

Het enige wat we hoeven te doen is de parameter type markeren met de reified trefwoord:

inline plezier Any.isA (): Boolean = dit is T

Zonder in lijn en reified, de is een functie zou niet compileren, zoals we uitvoerig uitleggen in ons Kotlin Generics-artikel.

3.4. Niet-lokale rendementen

In Kotlin, we kunnen de terugkeer expressie (ook bekend als niet-gekwalificeerd terugkeer) alleen om een ​​benoemde functie of een anonieme functie te verlaten:

fun namedFunction (): Int {return 42} fun anonymous (): () -> Int {// anonieme functie return fun (): Int {return 42}}

In beide voorbeelden is de terugkeer expressie is geldig omdat de functies een naam hebben of anoniem zijn.

Echter, we kunnen niet ongekwalificeerd gebruiken terugkeer expressies om een ​​lambda-expressie te verlaten. Laten we, om dit beter te begrijpen, nog een wiel opnieuw uitvinden:

fun List.eachIndexed (f: (Int, T) -> Unit) {for (i in indices) {f (i, this [i])}}

Deze functie voert het gegeven blok code uit (function f) op elk element, waarbij de sequentiële index met het element wordt verstrekt. Laten we deze functie gebruiken om een ​​andere functie te schrijven:

fun List.indexOf (x: T): Int {eachIndexed {index, value -> if (value == x) {return index}} return -1}

Deze functie moet het gegeven element op de ontvangende lijst doorzoeken en de index van het gevonden element of -1 teruggeven. Echter, omdat we een lambda niet kunnen verlaten met ongekwalificeerd terugkeer uitdrukkingen, zal de functie niet eens compileren:

Kotlin: 'retourneren' mag hier niet

Als tijdelijke oplossing voor deze beperking kunnen we dat in lijn de elk geïndexeerd functie:

inline leuke List.eachIndexed (f: (Int, T) -> Unit) {for (i in indices) {f (i, this [i])}}

Dan kunnen we de index van functie:

val gevonden = numbers.indexOf (5)

Inline-functies zijn slechts artefacten van de broncode en manifesteren zich niet tijdens runtime. Daarom het terugkeren van een inline lambda is gelijk aan het terugkeren van de omsluitende functie.

4. Beperkingen

Over het algemeen, we kunnen functies met lambda-parameters alleen inline uitvoeren als de lambda ofwel rechtstreeks wordt aangeroepen of wordt doorgegeven aan een andere inline-functie. Anders voorkomt de compiler inlinen met een compilatiefout.

Laten we bijvoorbeeld eens kijken naar het vervangen functie in de standaardbibliotheek van Kotlin:

inline fun CharSequence.replace (regex: Regex, noinline transform: (MatchResult) -> CharSequence): String = regex.replace (this, transform) // doorgeven aan een normale functie

Het fragment hierboven passeert de lambda, transformeren, naar een normale functie, vervangen, vandaar de noinline.

5. Conclusie

In dit artikel zijn we ingegaan op problemen met lambda-prestaties en het wissen van typen in Kotlin. Toen we inline-functies hadden geïntroduceerd, zagen we hoe deze beide problemen kunnen oplossen.

We moeten echter proberen om dit soort functies niet te veel te gebruiken, vooral als de body van de functie te groot is, aangezien de gegenereerde bytecode groter kan worden en we gaandeweg ook enkele JVM-optimalisaties kunnen verliezen.

Zoals gewoonlijk zijn alle voorbeelden beschikbaar op GitHub.