Funções embutidas no Kotlin

Funções embutidas no Kotlin

1. Visão geral

No Kotlin, as funções sãofirst-class citizens, então podemos passar funções ou retorná-las como outros tipos normais. No entanto, a representação dessas funções no tempo de execução às vezes pode causar algumas limitações ou complicações de desempenho.

Neste tutorial, primeiro vamos enumerar duas questões aparentemente não relacionadas sobre lambdas e genéricos e, em seguida, depois de apresentarInline Functions, veremos como eles podem resolver essas duas questões, então vamos começar!

2. Problemas no paraíso

2.1. A sobrecarga de Lambdas em Kotlin

Uma das vantagens de ser cidadãos de primeira classe em Kotlin é que podemos passar um pedaço de comportamento para outras funções. Passar funções como lambdas nos permite expressar nossas intenções de uma forma mais concisa e elegante, mas isso é apenas uma parte da história.

Para explorar o lado negro dos lambdas, vamos reinventar a roda declarando uma função de extensão para coleçõesfilter:

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

Agora, vamos ver como a função acima é compilada em Java. Concentre-se na funçãopredicate que está sendo passada como parâmetro:

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

Observe comopredicate é tratado usando a interfaceFunction1?

Agora, se chamarmos isso em Kotlin:

sampleCollection.filter { it == 1 }

Algo semelhante ao seguinte será produzido para quebrar o código 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.

Por que Kotlin faz isso em vez de, digamos, usarinvokedynamic like how Java 8 does with lambdas? Simply put, Kotlin goes for Java 6 compatibility, and invokedynamic isn’t available until Java 7.

Mas esse não é o fim. Como podemos imaginar, apenas criar uma instância de um tipo não é suficiente.

Para realmente executar a operação encapsulada em um lambda Kotlin, a função de ordem superior -filter neste caso - precisará chamar o método especial denominadoinvoke na nova instância. The result is more overhead due to the extra call.

Então, apenas para recapitular, quando estamos passando um lambda para uma função, o seguinte acontece nos bastidores:

  1. Pelo menos uma instância de um tipo especial é criada e armazenada na pilha

  2. Uma chamada de método extra sempre acontecerá

Mais uma alocação de instância e mais uma chamada de método virtualdoesn’t seem that bad, right?

2.2. Encerramentos

Como vimos anteriormente, quando passamos um lambda para uma função, uma instância de um tipo de função será criada, semelhante aanonymous inner classes em Java.

Assim como com o último, a lambda expression can access its closure, that is, variables declared in the outer scope. Quando um lambda captura uma variável de seu fechamento, Kotlin armazena a variável junto com o código de captura lambda.

The extra memory allocations get even worse when a lambda captures a variable: The JVM creates a function type instance on every invocation. Para lambdas sem captura, haverá apenas uma instância, asingleton, desses tipos de função.

Como temos tanta certeza disso? Vamos reinventar outra roda declarando uma função para aplicar uma função em cada elemento da coleção:

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

Por mais bobo que possa parecer, aqui vamos multiplicar cada elemento da coleção por um número aleatório:

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

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

E se dermos uma olhada dentro do bytecode usandojavap:

>> 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

Então, podemos detectar a partir do índice 51 que a JVM cria uma nova instância da classe internaMainKt$main$1 para cada chamada. Além disso, o índice 56 mostra como o Kotlin captura a variável aleatória. This means that each captured variable will be passed as constructor arguments, thus generating a memory overhead.

2.3. Tipo Erasure

Quando se trata de genéricos no JVM, nunca foi um paraíso, para começar! De qualquer forma, o Kotlin apaga as informações genéricas do tipo em tempo de execução. Ou seja,an instance of a generic class doesn’t preserve its type parameters at runtime.

Por exemplo, ao declarar algumas coleções comoList<Int> ouList<String>,, tudo o que temos em tempo de execução são apenasLists brutos. Isso parece não ter relação com os problemas anteriores, conforme prometido, mas veremos como as funções inline são a solução comum para ambos os problemas.

3. Funções Inline

3.1. Removendo a sobrecarga de lambdas

Ao usar lambdas, as alocações extras de memória e a chamada extra de método virtual introduzem alguma sobrecarga de tempo de execução. Portanto, se estivéssemos executando o mesmo código diretamente, em vez de usar lambdas, nossa implementação seria mais eficiente.

Temos que escolher entre abstração e eficiência?

As is turns out, with inline functions in Kotlin we can have both! Podemos escrever nossos lambdas bonitos e elegantes,and the compiler generates the inlined and direct code for us. Tudo o que precisamos fazer é colocar uminline nele:

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. Por padrão, o compilador alinha o código para a própria função e os lambdas passados ​​para ela.

Por exemplo, o compilador traduz:

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

Para algo como:

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.

No entanto, não devemos usar demais as funções embutidas, especialmente para funções longas, pois as embutidas podem fazer com que o código gerado cresça bastante.

3.2. Sem linha

Por padrão, todas as lambdas passadas para uma função embutida também seriam embutidas. No entanto, podemos marcar alguns dos lambdas com a palavra-chavenoinline para excluí-los do inlining:

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

3.3. Reificação em linha

Como vimos anteriormente, o Kotlin apaga as informações genéricas do tipo em tempo de execução, mas para funções em linha, podemos evitar essa limitação. That is, the compiler can reify generic type information for inline functions.

Tudo o que precisamos fazer é marcar o parâmetro de tipo com a palavra-chavereified :

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

Seminlineereified, a funçãoisA não compilaria, como explicamos detalhadamente em nosso artigoKotlin Generics.

4. Limitações

Geralmente,we can inline functions with lambda parameters only if the lambda is either called directly or passed to another inline function. , portanto, o compilador evita o inlining com um erro do compilador.

Por exemplo, vamos dar uma olhada na funçãoreplace na biblioteca padrão Kotlin:

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

O snippet acima passa o lambda,transform, para uma função normal,replace, portanto, onoinline.

5. Conclusão

Neste artigo, abordamos problemas com o desempenho lambda e apagamento de tipo no Kotlin. Depois de introduzir as funções embutidas, vimos como elas podem resolver os dois problemas.

No entanto, devemos tentar não usar excessivamente esse tipo de função, especialmente quando o corpo da função é muito grande, pois o tamanho do bytecode gerado pode aumentar e também podemos perder algumas otimizações da JVM ao longo do caminho.

Como de costume, os códigos de amostra estão disponíveis emour GitHub project, portanto, certifique-se de verificar!