Inline-funksjoner i Kotlin

1. Oversikt

I Kotlin er funksjoner førsteklasses borgere, så vi kan sende funksjoner rundt eller returnere dem akkurat som andre normale typer. Imidlertid kan representasjonen av disse funksjonene ved kjøretid noen ganger føre til noen få begrensninger eller ytelseskomplikasjoner.

I denne opplæringen skal vi først oppregne to tilsynelatende ikke-relaterte problemer om lambdas og generiske stoffer, og deretter, etter å ha introdusert Inline-funksjoner, vi får se hvordan de kan takle begge disse bekymringene, så la oss komme i gang!

2. Trøbbel i paradis

2.1. Overhead of Lambdas i Kotlin

En av fordelene ved funksjoner som førsteklasses borgere i Kotlin er at vi kan overføre en oppførsel til andre funksjoner. Passering fungerer som lambdas, la oss uttrykke våre intensjoner på en mer kortfattet og elegant måte, men det er bare en del av historien.

For å utforske den mørke siden av lambdas, la oss finne opp hjulet på nytt ved å erklære en utvidelsesfunksjon til filter samlinger:

morsom Collection.filter (predikat: (T) -> Boolean): Collection = // Utelatt

La oss nå se hvordan funksjonen ovenfor kompileres til Java. Fokuser på predikat funksjon som sendes som parameter:

offentlig statisk endelig Samlingsfilter (Collection, kotlin.jvm.functions.Function1);

Legg merke til hvordan predikat håndteres ved hjelp av Funksjon 1 grensesnitt?

Nå, hvis vi kaller dette i Kotlin:

sampleCollection.filter {it == 1}

Noe som ligner på følgende vil bli produsert for å pakke inn lambdakoden:

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

Hver gang vi erklærer en høyere ordensfunksjon, minst én forekomst av de spesielle Funksjon* typer vil bli opprettet.

Hvorfor gjør Kotlin dette i stedet for å si påkalt dynamisk liker hvordan Java 8 gjør med lambdas? Enkelt sagt, Kotlin går for Java 6-kompatibilitet, og påkalt dynamisk er ikke tilgjengelig før Java 7.

Men dette er ikke slutten på det. Som vi kanskje gjetter, er det ikke nok å lage en forekomst av en type.

For å faktisk utføre operasjonen innkapslet i en Kotlin lambda, er den høyere ordensfunksjonen - filter i dette tilfellet - må ringe den spesielle metoden som heter påkalle på den nye forekomsten. Resultatet er mer overhead på grunn av den ekstra samtalen.

Så for å oppsummere, når vi passerer en lambda til en funksjon, skjer følgende under panseret:

  1. Minst en forekomst av en spesiell type opprettes og lagres i dyngen
  2. En ekstra metodeanrop vil alltid skje

En ekstra forekomstallokering og en mer virtuell metodeanrop virker ikke så ille, ikke sant?

2.2. Stengninger

Som vi så tidligere, når vi sender en lambda til en funksjon, vil en forekomst av en funksjonstype opprettes, i likhet med anonyme indre klasser i Java.

Akkurat som med sistnevnte, et lambdauttrykk kan få tilgang til det nedleggelsedet vil si variabler som er deklarert i det ytre omfanget. Når en lambda fanger en variabel fra lukkingen, lagrer Kotlin variabelen sammen med fangstlambdakoden.

De ekstra minnetildelingene blir enda verre når en lambda fanger en variabel: JVM oppretter en funksjonstypeforekomst på hver påkalling. For ikke-fangende lambdas vil det bare være en forekomst, a singleton, av disse funksjonstypene.

Hvordan er vi så sikre på dette? La oss finne et nytt hjul på nytt ved å erklære at en funksjon skal bruke en funksjon på hvert innsamlingselement:

morsom Collection.each (blokk: (T) -> Enhet) {for (e i denne) blokk (e)}

Så dumt det kan høres ut, her skal vi multiplisere hvert innsamlingselement med et tilfeldig tall:

morsomme hoved () {val tall = listeOf (1, 2, 3, 4, 5) val tilfeldig = tilfeldig () tall. hvert {println (tilfeldig * it)} // fange den tilfeldige variabelen}

Og hvis vi tar en titt inn i bytekoden ved hjelp av javap:

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

Så kan vi se fra indeks 51 som JVM oppretter en ny forekomst av MainKt $ main $1 indre klasse for hver påkallelse. Indeks 56 viser også hvordan Kotlin fanger opp den tilfeldige variabelen. Dette betyr at hver fanget variabel vil bli sendt som konstruktørargumenter, og dermed genererer et minneoverhead.

2.3. Skriv sletting

Når det gjelder generiske produkter på JVM, har det aldri vært et paradis, til å begynne med! Uansett sletter Kotlin den generiske typen informasjon under kjøretid. Det er, en forekomst av en generisk klasse bevarer ikke typeparametrene på kjøretid.

For eksempel når deklarerer noen få samlinger som Liste eller Liste, alt vi har på kjøretid er bare rå Listes. Dette virker ikke relatert til de tidligere utgavene, som lovet, men vi får se hvordan innebygde funksjoner er den vanlige løsningen for begge problemene.

3. Inline-funksjoner

3.1. Fjerne overhead av Lambdas

Når du bruker lambdas, introduserer ekstra minnetildelinger og ekstra virtuell metodesamtale noe runtime overhead. Så hvis vi utførte den samme koden direkte, i stedet for å bruke lambdas, ville implementeringen vår være mer effektiv.

Må vi velge mellom abstraksjon og effektivitet?

Som det viser seg, med innebygde funksjoner i Kotlin kan vi ha begge deler! Vi kan skrive våre fine og elegante lambdas, og kompilatoren genererer den innebygde og direkte koden for oss. Alt vi trenger å gjøre er å sette en på linje på den:

inline fun Collection.each (blokk: (T) -> Enhet) {for (e i denne) blokk (e)}

Når du bruker innebygde funksjoner, integrerer kompilatoren funksjonen. Det vil si at den erstatter kroppen direkte til steder der funksjonen blir kalt. Som standard legger kompilatoren inn koden for både selve funksjonen og lambdasene som sendes til den.

For eksempel oversetter kompilatoren:

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

Til noe sånt som:

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

Når du bruker innebygde funksjoner, er det ingen ekstra objektallokering og ingen ekstra virtuelle metodeanrop.

Vi bør imidlertid ikke overbruke de innebygde funksjonene, spesielt ikke for lange funksjoner, siden innfellingen kan føre til at den genererte koden vokser ganske mye.

3.2. Ingen innebygd

Som standard vil også alle lambdas som sendes til en innebygd funksjon være inline. Imidlertid kan vi merke noen av lambdas med noinline nøkkelord for å utelukke dem fra inlining:

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

3.3. Inline Reification

Som vi så tidligere, sletter Kotlin den generiske typen informasjon under kjøretid, men for integrerte funksjoner kan vi unngå denne begrensningen. Det vil si at kompilatoren kan reifisere generisk typeinformasjon for innebygde funksjoner.

Alt vi trenger å gjøre er å merke typeparameteren med reifisert nøkkelord:

inline moro Any.isA (): Boolsk = dette er T

Uten på linje og reifisert, den er en funksjon ikke ville kompilere, som vi grundig forklarer i vår Kotlin Generics-artikkel.

3.4. Ikke-lokale retur

I Kotlin, vi kan bruke komme tilbake uttrykk (også kjent som ukvalifisert komme tilbake) bare for å gå ut av en navngitt funksjon eller en anonym funksjon:

moro med navnFunksjon (): Int {retur 42} morsom anonym (): () -> Int {// anonym funksjon returner moro (): Int {retur 42}}

I begge eksemplene er komme tilbake uttrykk er gyldig fordi funksjonene enten er navngitte eller anonyme.

Derimot, vi kan ikke bruke ukvalifisert komme tilbake uttrykk for å gå ut av et lambdauttrykk. For å forstå dette bedre, la oss finne ut nok et hjul:

morsom List.eachIndexed (f: (Int, T) -> Enhet) {for (i i indekser) {f (i, dette [i])}}

Denne funksjonen utfører den gitte kodeblokken (funksjon f) på hvert element, og gir den sekvensielle indeksen med elementet. La oss bruke denne funksjonen til å skrive en annen funksjon:

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

Denne funksjonen skal søke i det gitte elementet på mottakerlisten og returnere indeksen til det funnet elementet eller -1. Derimot, siden vi ikke kan gå ut av en lambda med ukvalifisert komme tilbake uttrykk, vil ikke funksjonen engang kompilere:

Kotlin: 'retur' er ikke tillatt her

Som en løsning for denne begrensningen, kan vi på linje de eachIndexed funksjon:

inline fun List.eachIndexed (f: (Int, T) -> Unit) {for (i i indekser) {f (i, dette [i])}}

Da kan vi faktisk bruke oversikt over funksjon:

val funnet = numbers.indexOf (5)

Inline-funksjoner er bare gjenstander av kildekoden og manifesterer seg ikke ved kjøretid. Derfor, retur fra en innrammet lambda tilsvarer retur fra inneslutningsfunksjonen.

4. Begrensninger

Som regel, vi kan bare integrere funksjoner med lambda-parametere hvis lambda enten blir ringt direkte eller sendt til en annen inline-funksjon. Ellers forhindrer kompilatoren inline med en kompilatorfeil.

La oss for eksempel se på erstatte funksjon i Kotlin standardbibliotek:

inline fun CharSequence.replace (regex: Regex, noinline transform: (MatchResult) -> CharSequence): String = regex.replace (this, transform) // overføring til en normal funksjon

Utdraget over passerer lambda, forvandle, til en normal funksjon, erstatte, derav noinline.

5. Konklusjon

I denne artikkelen dykker vi inn i problemer med lambda-ytelse og type sletting i Kotlin. Så, etter å ha introdusert innebygde funksjoner, så vi hvordan disse kan løse begge problemene.

Vi bør imidlertid prøve å ikke overbruke denne typen funksjoner, spesielt når funksjonsdelen er for stor ettersom den genererte bykodestørrelsen kan vokse, og vi kan også miste noen få JVM-optimaliseringer underveis.

Som vanlig er alle eksemplene tilgjengelige på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found