quando usar uma função embutida no Kotlin?

105

Eu sei que uma função embutida pode melhorar o desempenho e fazer com que o código gerado cresça, mas não tenho certeza de quando é correto usar uma.

lock(l) { foo() }

Em vez de criar um objeto de função para o parâmetro e gerar uma chamada, o compilador pode emitir o código a seguir. ( Fonte )

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

mas descobri que não há nenhum objeto de função criado por kotlin para uma função não embutida. porque?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}
holi-java
fonte
7
Existem dois casos de uso principais para isso, um é com certos tipos de funções de ordem superior e o outro são parâmetros de tipo reificado. A documentação das funções inline cobre: kotlinlang.org/docs/reference/inline-functions.html
zsmb13
2
@ zsmb13 obrigado, senhor. mas eu não entendo que: "Em vez de criar um objeto de função para o parâmetro e gerar uma chamada, o compilador poderia emitir o seguinte código"
holi-java
2
Eu não entendo esse exemplo também tbh.
filthy_wizard

Respostas:

279

Digamos que você crie uma função de ordem superior que recebe um lambda do tipo () -> Unit(sem parâmetros, sem valor de retorno) e o executa assim:

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

Na linguagem Java, isso se traduzirá em algo assim (simplificado!):

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

E quando você liga de Kotlin ...

nonInlined {
    println("do something here")
}

Nos bastidores, uma instância de Functionserá criada aqui, que envolve o código dentro do lambda (novamente, isso é simplificado):

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

Então, basicamente, chamar essa função e passar um lambda para ela sempre criará uma instância de um Functionobjeto.


Por outro lado, se você usar a inlinepalavra-chave:

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

Quando você chama assim:

inlined {
    println("do something here")
}

Nenhuma Functioninstância será criada; em vez disso, o código em torno da invocação de blockdentro da função embutida será copiado para o site da chamada, então você obterá algo assim no bytecode:

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

Nesse caso, nenhuma nova instância é criada.

zsmb13
fonte
19
Qual é a vantagem de ter o wrapper do objeto Function em primeiro lugar? ou seja - por que nem tudo está alinhado?
Arturs Vancans
14
Desta forma, você também pode passar funções arbitrariamente como parâmetros, armazená-los em variáveis, etc.
zsmb13
6
Ótima explicação por @ zsmb13
Yajairo87
2
Você pode, e se fizer coisas complicadas com eles, acabará querendo saber sobre as palavras-chave noinlinee crossinline- consulte os documentos .
zsmb13
2
Os documentos fornecem um motivo pelo qual você não gostaria de embutir
CorayThan
43

Deixe-me adicionar: "Quando não usar inline" :

1) Se você tem uma função simples que não aceita outras funções como argumento, não faz sentido embuti-las. O IntelliJ irá avisá-lo:

O impacto esperado no desempenho do inlining '...' é insignificante. Inlining funciona melhor para funções com parâmetros de tipos funcionais

2) Mesmo se você tiver uma função "com parâmetros de tipos funcionais", você pode encontrar o compilador dizendo que o inlining não funciona. Considere este exemplo:

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

Este código não compilará com o erro:

Uso ilegal de 'operação' de parâmetro embutido em '...'. Adicione o modificador 'noinline' à declaração do parâmetro.

O motivo é que o compilador não consegue embutir esse código. Se operationnão estiver envolvido em um objeto (o que está implícito, inlinevisto que você deseja evitar isso), como pode ser atribuído a uma variável? Nesse caso, o compilador sugere fazer o argumento noinline. Ter uma inlinefunção com uma única noinlinefunção não faz sentido, não faça isso. No entanto, se houver vários parâmetros de tipos funcionais, considere embutir alguns deles, se necessário.

Então, aqui estão algumas regras sugeridas:

  • Você pode embutir quando todos os parâmetros de tipo funcional são chamados diretamente ou passados ​​para outra função embutida
  • Você deve embutir quando ^ for o caso.
  • Você não pode embutir quando o parâmetro da função está sendo atribuído a uma variável dentro da função
  • Você deve considerar embutir se pelo menos um dos seus parâmetros de tipo funcional puder ser embutido, use noinlinepara os outros.
  • Você não deve embutir funções enormes, pense no código de bytes gerado. Ele será copiado para todos os locais de onde a função é chamada.
  • Outro caso de uso são os reifiedparâmetros de tipo, que exigem seu uso inline. Leia aqui .
s1m0nw1
fonte
4
tecnicamente, você ainda poderia funções inline que não usam expressões lambda certas? .. aqui a vantagem é que a sobrecarga de chamada de função é evitada nesse caso .. linguagens como Scala permitem isso .. não tenho certeza por que Kotlin proíbe esse tipo de inline- ing
rogue-one
3
@ rogue-one Kotlin não proíbe este momento de inlining. Os autores da linguagem estão simplesmente afirmando que o benefício de desempenho provavelmente será insignificante. É provável que métodos pequenos já sejam sequenciados pela JVM durante a otimização JIT, especialmente se forem executados com frequência. Outro caso em que inlinepode ser prejudicial é quando o parâmetro funcional é chamado várias vezes na função embutida, como em diferentes ramificações condicionais. Acabei de encontrar um caso em que todo o bytecode para argumentos funcionais estava sendo duplicado por causa disso.
Mike Hill
5

O caso mais importante quando usamos o modificador embutido é quando definimos funções do tipo utilitário com funções de parâmetro. O processamento de coleção ou string (como filter, mapou joinToString) ou apenas funções autônomas são um exemplo perfeito.

É por isso que o modificador embutido é principalmente uma otimização importante para desenvolvedores de bibliotecas. Eles devem saber como funciona e quais são suas melhorias e custos. Devemos usar o modificador embutido em nossos projetos quando definimos nossas próprias funções util com parâmetros de tipo de função.

Se não tivermos parâmetro de tipo de função, parâmetro de tipo reificado e não precisarmos de retorno não local, provavelmente não devemos usar o modificador embutido. É por isso que teremos um aviso no Android Studio ou IDEA IntelliJ.

Além disso, há um problema de tamanho do código. O inlining de uma função grande pode aumentar drasticamente o tamanho do bytecode porque ele é copiado para cada site de chamada. Nesses casos, você pode refatorar a função e extrair o código para funções regulares.

0xAliHn
fonte
4

As funções de ordem superior são muito úteis e podem realmente melhorar o funcionamento reusabilitydo código. No entanto, uma das maiores preocupações em usá-los é a eficiência. As expressões lambda são compiladas em classes (geralmente classes anônimas), e a criação de objetos em Java é uma operação pesada. Ainda podemos usar funções de ordem superior de forma eficaz, mantendo todos os benefícios, tornando as funções embutidas.

aqui vem a função embutida em imagem

Quando uma função é marcada como inline, durante a compilação do código, o compilador substituirá todas as chamadas de função pelo corpo real da função. Além disso, as expressões lambda fornecidas como argumentos são substituídas por seu corpo real. Eles não serão tratados como funções, mas como código real.

Resumindo: - Inline -> em vez de serem chamados, eles são substituídos pelo código do corpo da função em tempo de compilação ...

Em Kotlin, usar uma função como parâmetro de outra função (as chamadas funções de ordem superior) parece mais natural do que em Java.

Usar lambdas tem algumas desvantagens, entretanto. Como são classes anônimas (e, portanto, objetos), elas precisam de memória (e podem até aumentar a contagem geral de métodos do seu aplicativo). Para evitar isso, podemos embutir nossos métodos.

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

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

Do exemplo acima : - Essas duas funções fazem exatamente a mesma coisa - imprimir o resultado da função getString. Um está embutido e o outro não.

Se você verificar o código java descompilado, verá que os métodos são completamente idênticos. Isso ocorre porque a palavra-chave inline é uma instrução para o compilador copiar o código para o site de chamada.

No entanto, se estivermos passando qualquer tipo de função para outra função como abaixo:

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

Para resolver isso, podemos reescrever nossa função conforme abaixo:

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

Suponha que tenhamos uma função de ordem superior como abaixo:

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

Aqui, o compilador nos dirá para não usar a palavra-chave inline quando houver apenas um parâmetro lambda e o estivermos passando para outra função. Portanto, podemos reescrever a função acima como abaixo:

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

Nota : - tivemos que remover a palavra-chave noinline também porque ela só pode ser usada para funções inline!

Suponha que tenhamos uma função como esta ->

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

Isso funciona bem, mas a essência da lógica da função está poluída com o código de medição, tornando mais difícil para seus colegas trabalharem o que está acontecendo. :)

Veja como uma função embutida pode ajudar neste código:

      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
    }

Agora posso me concentrar em ler qual é a principal intenção da função intercept () sem pular as linhas do código de medição. Também nos beneficiamos da opção de reutilizar esse código em outros lugares onde queremos

inline permite que você chame uma função com um argumento lambda dentro de um encerramento ({...}) em vez de passar a medida lambda like (myLamda)

Wini
fonte
2

Um caso simples onde você pode querer um é quando você cria uma função util que leva em um bloco suspenso. Considere isto.

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

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

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

Nesse caso, nosso cronômetro não aceitará funções de suspensão. Para resolver isso, você pode ficar tentado a suspendê-lo também

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

Mas então ele só pode ser usado a partir das próprias co-rotinas / funções suspensas. Então, você acabará fazendo uma versão assíncrona e uma versão não assíncrona desses utilitários. O problema desaparece se você torná-lo embutido.

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

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

    launch {
        timer { asyncLogic() }
    }
}

Aqui está um playground kotlin com o estado de erro. Faça o cronômetro embutido para resolvê-lo.

Anthony Naddeo
fonte