Fonctions en ligne dans Kotlin

Fonctions en ligne dans Kotlin

1. Vue d'ensemble

Dans Kotlin, les fonctions sontfirst-class citizens, donc nous pouvons passer des fonctions ou les renvoyer comme les autres types normaux. Cependant, la représentation de ces fonctions au moment de l'exécution peut parfois entraîner quelques limitations ou complications de performances.

Dans ce didacticiel, nous allons d'abord énumérer deux problèmes apparemment sans rapport avec les lambdas et les génériques, puis, après avoir présentéInline Functions, nous verrons comment ils peuvent résoudre ces deux problèmes, alors commençons!

2. Trouble au paradis

2.1. Les frais généraux de Lambdas à Kotlin

Un des avantages des fonctions en tant que citoyens de premier plan à Kotlin est que nous pouvons transmettre un élément de comportement à d'autres fonctions. Passer des fonctions comme des lambdas nous permet d'exprimer nos intentions de manière plus concise et élégante, mais ce n'est qu'une partie de l'histoire.

Pour explorer le côté obscur des lambdas, réinventons la roue en déclarant une fonction d'extension aux collectionsfilter:

fun  Collection.filter(predicate: (T) -> Boolean): Collection = // Omitted

Voyons maintenant comment la fonction ci-dessus se compile en Java. Focus sur la fonctionpredicate qui est passée en paramètre:

public static final  Collection filter(Collection, kotlin.jvm.functions.Function1);

Remarquez comment lespredicate sont gérés en utilisant l'interfaceFunction1?

Maintenant, si nous appelons cela à Kotlin:

sampleCollection.filter { it == 1 }

Quelque chose de semblable au suivant sera produit pour envelopper le code lambda:

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

Every time we declare a higher-order function, at least one instance of those special Function* types will be created.

Pourquoi Kotlin fait-il cela au lieu, disons, d'utiliserinvokedynamic like how Java 8 does with lambdas? Simply put, Kotlin goes for Java 6 compatibility, and invokedynamic isn’t available until Java 7.

Mais ce n'est pas la fin. Comme on peut l’imaginer, il ne suffit pas de créer une instance d’un type.

Afin d'effectuer réellement l'opération encapsulée dans un lambda Kotlin, la fonction d'ordre supérieur -filter dans ce cas - devra appeler la méthode spéciale nomméeinvoke sur la nouvelle instance. The result is more overhead due to the extra call.

Donc, pour récapituler, lorsque nous transmettons un lambda à une fonction, ce qui suit se passe sous le capot:

  1. Au moins une instance d'un type spécial est créée et stockée dans le tas

  2. Un appel de méthode supplémentaire aura toujours lieu

Une allocation d'instance supplémentaire et un autre appel de méthode virtuelledoesn’t seem that bad, right?

2.2. Fermetures

Comme nous l'avons vu précédemment, lorsque nous passons un lambda à une fonction, une instance d'un type de fonction sera créée, similaire àanonymous inner classes en Java.

Tout comme avec ce dernier, a lambda expression can access its closure, that is, variables declared in the outer scope. Lorsqu'un lambda capture une variable à partir de sa fermeture, Kotlin stocke la variable avec le code lambda de capture.

The extra memory allocations get even worse when a lambda captures a variable: The JVM creates a function type instance on every invocation. Pour les lambdas non capturants, il n'y aura qu'une seule instance, unsingleton, de ces types de fonction.

Comment en sommes-nous si sûrs? Réinventons une autre roue en déclarant une fonction pour appliquer une fonction à chaque élément de collection:

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

Aussi ridicule que cela puisse paraître, ici nous allons multiplier chaque élément de collection par un nombre aléatoire:

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val random = random()

    numbers.each { println(random * it) } // capturing the random variable
}

Et si nous jetons un coup d'œil à l'intérieur du bytecode en utilisantjavap:

>> javap -c MainKt
public final class MainKt {
  public static final void main();
    Code:
      // Omitted
      51: new           #29                 // class MainKt$main$1
      54: dup
      55: fload_1
      56: invokespecial #33                 // Method 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

Ensuite, nous pouvons repérer à partir de l'index 51 que la JVM crée une nouvelle instance de la classe interneMainKt$main$1 pour chaque appel. En outre, l'index 56 montre comment Kotlin capture la variable aléatoire. This means that each captured variable will be passed as constructor arguments, thus generating a memory overhead.

2.3. Type d'effacement

En ce qui concerne les génériques sur la JVM, cela n’a jamais été un paradis, pour commencer! Quoi qu'il en soit, Kotlin efface les informations de type génériques au moment de l'exécution. Autrement dit,an instance of a generic class doesn’t preserve its type parameters at runtime.

Par exemple, lors de la déclaration de quelques collections telles queList<Int> ouList<String>,, tout ce que nous avons à l'exécution n'est que desLists bruts. Cela ne semble pas lié aux problèmes précédents, comme promis, mais nous verrons comment les fonctions en ligne sont la solution commune aux deux problèmes.

3. Fonctions en ligne

3.1. Suppression des frais généraux de Lambdas

Lors de l'utilisation de lambdas, l'allocation de mémoire supplémentaire et l'appel de méthode virtuelle supplémentaire introduisent une surcharge de temps d'exécution. Donc, si nous exécutions directement le même code, au lieu d’utiliser lambdas, notre implémentation serait plus efficace.

Faut-il choisir entre abstraction et efficacité?

As is turns out, with inline functions in Kotlin we can have both! Nous pouvons écrire nos jolis et élégants lambdas,and the compiler generates the inlined and direct code for us. Tout ce que nous avons à faire est de mettre uninline dessus:

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

When using inline functions, the compiler inlines the function body. That is, it substitutes the body directly into places where the function gets called. Par défaut, le compilateur intègre le code pour la fonction elle-même et les lambdas qui lui sont passés.

Par exemple, le compilateur traduit:

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

À quelque chose comme:

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

When using inline functions, there is no extra object allocation and no extra virtual method calls.

Cependant, nous ne devrions pas abuser des fonctions inline, en particulier pour les fonctions longues, car l’inline pourrait entraîner une croissance importante du code généré.

3.2. Aucun inline

Par défaut, tous les lambdas passés à une fonction inline seraient également en ligne. Cependant, nous pouvons marquer certains des lambdas avec le mot clénoinline pour les exclure de l'inlining:

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

3.3. Reification en ligne

Comme nous l'avons vu précédemment, Kotlin efface les informations de type génériques au moment de l'exécution, mais pour les fonctions en ligne, nous pouvons éviter cette limitation. That is, the compiler can reify generic type information for inline functions.

Tout ce que nous avons à faire est de marquer le paramètre type avec le skeywordreified :

inline fun  Any.isA(): Boolean = this is T

Sansinline etreified, la fonctionisA ne se compilerait pas, comme nous l'expliquons en détail dans notre article surKotlin Generics.

4. Limitations

En général,we can inline functions with lambda parameters only if the lambda is either called directly or passed to another inline function.  Sinon, le compilateur empêche l'inlining avec une erreur du compilateur.

Par exemple, examinons la fonctionreplace dans la bibliothèque standard Kotlin:

inline fun CharSequence.replace(regex: Regex, noinline transform: (MatchResult) -> CharSequence): String =
    regex.replace(this, transform) // passing to a normal function

L'extrait de code ci-dessus passe le lambda,transform, à une fonction normale,replace, d'où lenoinline.

5. Conclusion

Dans cet article, nous nous sommes plongés dans des problèmes de performances lambda et d'effacement de types dans Kotlin. Ensuite, après avoir introduit les fonctions inline, nous avons vu comment celles-ci peuvent résoudre ces deux problèmes.

Cependant, nous devrions essayer de ne pas abuser de ce type de fonctions, en particulier lorsque le corps de la fonction est trop volumineux car la taille du bytecode générée peut augmenter et que nous risquons également de perdre quelques optimisations JVM en cours de route.

Comme d'habitude, les exemples de codes sont disponibles surour GitHub project, alors assurez-vous de le vérifier!