Insights sobre performance em Kotlin: Impactos de Lambdas, Overhead e Garbage Collector

Renan Lima
4 min readOct 8, 2024

--

Talvez você já tenha ouvido alguém falando sobre codar Kotlin com sotaque Java — Essa expressão refere-se ao fato de escrever código Kotlin usando padrões e estilos comuns em Java, o que muitas vezes impede a exploração dos recursos mais avançados que aquela linguagem oferece. — E de fato essa tem sido a realidade de muitos desenvolvedores durante a migração para Kotlin.

Um dos grandes diferenciais em Kotlin é o possibilidade de usar lambdas para quase tudo o que vamos fazer e é sobre isso que gostaria de fazer uma análise um pouco mais aprofundada sobre o que ele representa em termos de performance. Vamos fazer uma análise do relacionamento de lambdas, os overheads associados a ele e como esse relacionamento impacta no garbage collector. Entendê-los pode ser crucial no desenvolvimento de uma aplicação.

O que é Lambda?

Funções lambdas são conhecidas como funções anônimas(anonymous functions ou literal functions). São funções que não possuem nome e são passadas como expressões a outras funções.

A sintaxe básica do lambda é a seguinte:

Ex:.
// Definindo uma lambda que soma dois números
val sum: (Int, Int) -> Int = { a, b -> a + b }
OU
val sum = { a: Int, b: Int -> a + b }

// Definindo uma lambda que multiplica dois números
val multiply: (Int,Int) -> Int = { a, b -> a * b}
OU
val multiply = { a: Int, b: Int -> a * b}

Nos exemplos acima temos duas formas distintas de criarmos um lambda, mas em regras gerais estamos definindo um lambda que aceita dois parâmetros(a e b) do tipo Int e que retorna a soma deles. O que separada os parâmetros do corpo da função é a ->. A partir de então podemos chamá-lo como qualquer outra função:

sum(3,4)
multiply(3,4)

Lambdas e Overheads

Antes de entendermos como os lambdas se comportam em Kotlin, gostaria de fazer uma abordagem conceitual mais genérica.

O uso constante de lambdas pode gerar um alto impacto na performance de uma aplicação. Isso acontece porque na prática cada lambda é instanciado, sendo representado como objeto e alocado na memória.

Isso quer dizer que o código lambda (Int, Int) -> Int = { a, b -> a + b } por detrás dos panos é inserido dentro de uma classe anônima e alocado na memória. Dessa forma o compilador está gerando código adicional e consumindo memória para armazenar essas instâncias.

Para resolver esse problema e reduzir os overheads a linguagem Kotlin faz uso constante de funções inline, como vemos nesses exemplos abaixo:

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}

public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}

Quando uma função é marcada como inline, o compilador substitui aquela chamada de função pelo próprio código da função, dessa forma a necessidade de se armazenar uma instância na memória desaparece. Portanto, quando fazemos uso da função filter em uma Collection a mágica que acontece é:
- A chamada da função filter é substituída pelo próprio código de filterTo diretamente.
- A comparação element.id == objectId é inserida no trecho do loop

...
/* Essa linha representa uma função de exemplo que itera uma lista "objects" e filtra um determinado elemento
pelo ID
*/
//O que está em código visível
fun filterSingleObject(objectId: UUID) = objects.filter { it.id == objectId}

...

//O que o compilador gera
fun filterSingleObject(iId: UUID): List<T> {
val destination = ArrayList<T>()
for (element in ins) {
if (element.id == objectId) {
destination.add(element)
}
}
return destination
}

Garbage Collection e seu relacionamento com lambdas

A JVM(Java Virtual Machine) faz uso de sistema de gestão de memória chamado Garbage Collection sendo seu ator principal o GC(Garbage Collector). Sua função principal é verificar quais objetos não estão mais sendo usados e removê-los da memória HEAP.
De fato o GC trouxe esse grande diferencial, já que programadores de C e C++ tinham a árdua tarefa de fazê-lo manualmente, o que aumentava o risco de memory leaks ou dangling pointers.

Visto que Kotlin possui interoperabilidade com Java e roda sobre a JVM, o gerenciamento de memória acaba seguindo as mesmas regras que segue com JAVA. Dessa forma Kotlin acaba tendo uma grande vantagem em seus lambdas devido ao uso de inline functions , como dito anteriormente. Se lambdas não forem marcados como inline, haverá uma criação maior de objetos temporários que, mesmo que sejam coletados pelo GC, podem aumentar a carga de trabalho dele, resultando então em mais pausas e menor desempenho.

De qualquer forma, é preciso ter-se em mente que fazer uso de funções lambdas que não sejam inline poderá acarretar em uma grande sobrecarga ao GC e sua gestão de memória e consequentemente perda na performance da aplicação.

--

--

No responses yet