Kotlin: Otimização de Performance Kotlin Explicada
Última atualização: 2026-08-26
Performance não é otimização prematura — é fazer as escolhas certas: Charlie substitui cadeias de List por Sequence para evitar alocações de coleções intermediárias para milhões de pedidos, usa IntArray em vez de Array<Int> para eliminar boxing, e valida cada otimização com JMH.
1. O que Você Aprenderá
- Funções
inline: eliminam alocação de objetos lambda - Seleção de coleções:
ListvsSequencevsFlowvsArray - Ajuste de dispatcher de corrotinas
- Arrays de tipos primitivos:
IntArrayvsArray<Int> - Charlie na prática: otimização de throughput para milhão de pedidos
2. A História Real de um Arquiteto
(1) Problema: Out of Memory Processando um Milhão de Pedidos
O OrderProcessor do Charlie criava uma nova coleção a cada passo ao processar um milhão de pedidos — pico de memória atingiu 4GB, pausas de GC chegaram a 2 segundos.
(2) Solução de Otimização de Performance
// Antes: 3 coleções intermediárias, 4GB de pico de memória
orders.map { enrich(it) }.filter { it.total > 100 }.groupBy { it.region }
// Depois: 0 coleções intermediárias, 200MB de pico de memória
orders.asSequence()
.map { enrich(it) }
.filter { it.total > 100 }
.groupBy { it.region } // Apenas 1 coleção final
Avaliação preguiçosa com Sequence + arrays de tipos primitivos + ajuste de pool de corrotinas = 95% de redução de memória, 10x melhoria de throughput.
3. Funções Inline Eliminam Overhead
(1) O Custo Oculto das Lambdas
// Não-inline: Lambda compila para classe anônima
fun process(order: Order, block: (Order) -> String): String {
return block(order) // Cria objeto Function1 a cada chamada
}
// Inline: Código da lambda é inlineado no local da chamada
inline fun process(order: Order, block: (Order) -> String): String {
return block(order) // Sem alocação de objeto!
}
(2) Medindo o Efeito do Inline
// Micro-benchmark JMH (conceitual)
// Não-inline: ~50ns por chamada (alocação de objeto)
// Inline: ~5ns por chamada (sem alocação, JIT faz inline adicional)
(3) Diretrizes de Uso do Inline
| Cenário | Recomendação | Motivo |
|---|---|---|
| Função de ordem superior (corpo de 1-5 linhas) | ✅ inline | Elimina alocação de objeto lambda |
| Parâmetro de tipo reified | ✅ deve usar inline | Preserva tipo em tempo de compilação |
| Corpo de função grande (>20 linhas) | ❌ não usar inline | Aumenta tamanho do código |
| Lambda armazenada/passada adiante | ❌ não usar inline | Lambda inlineado deixa de existir |
4. Seleção de Coleções
(1) Quatro Abordagens de Processamento de Dados
// 1. List (Eager): cada passo cria nova coleção
val result1 = orders
.map { enrich(it) } // Nova List
.filter { it.total > 100 } // Nova List
.toList() // Nova List
// 2. Sequence (Lazy): processa um elemento por vez
val result2 = orders.asSequence()
.map { enrich(it) }
.filter { it.total > 100 }
.toList() // Apenas 1 List
// 3. Flow (Async Lazy): stream preguiçoso com suporte a suspend
val result3 = orders.asFlow()
.map { enrichAsync(it) } // Pode ser suspend
.filter { it.total > 100 }
.toList() // Apenas 1 List
// 4. Array: menor overhead, tamanho fixo
val result4 = ordersArray
.map { enrich(it) } // Cria novo Array
.filter { it.total > 100 } // Sem filter em Array
(2) Árvore de Decisão para Seleção de Coleções
flowchart LR
A[Processamento de Dados] --> B{Precisa de async?}
B -->|Sim| C[Flow]
B -->|Não| D{Tamanho dos dados?}
D -->|Grande >10K| E{Múltiplos passos?}
D -->|Pequeno <10K| F[List]
E -->|Sim| G[Sequence]
E -->|Não| F
A --> H{Tipos primitivos?}
H -->|Sim| I[IntArray/DoubleArray]
H -->|Não| A
(3) Comparação de Performance
| Abordagem | Memória | CPU | Melhor Para |
|---|---|---|---|
List |
O(n×passos) | Baixa (cache-friendly) | Dados pequenos, poucos passos |
Sequence |
O(1) | Média (pipeline completo por elemento) | Dados grandes, muitos passos |
Flow |
O(1) | Média + overhead async | Fontes de dados assíncronas |
Array |
O(n) | Mais baixa (sem boxing) | Primitivos, tamanho fixo |
5. Arrays de Tipos Primitivos
(1) IntArray vs Array<Int>
// Array<Int>: cada elemento é um objeto Integer encapsulado (boxed)
val boxed: Array<Int> = Array(1_000_000) { it }
// Memória: ~24MB (4MB dados + 20MB headers de objetos)
// IntArray: int[] primitivo, sem boxing
val primitive: IntArray = IntArray(1_000_000) { it }
// Memória: ~4MB (sem overhead de objeto)
// 6x de economia de memória com arrays primitivos!
(2) Seleção de Array Primitivo
| Tipo | Array Boxed | Array Primitivo | Economia |
|---|---|---|---|
| Int | Array<Int> |
IntArray |
~6x |
| Long | Array<Long> |
LongArray |
~6x |
| Double | Array<Double> |
DoubleArray |
~6x |
| Boolean | Array<Boolean> |
BooleanArray |
~8x |
6. Ajuste de Dispatcher de Corrotinas
(1) Seleção de Dispatcher
// Default: CPU-bound (paralelismo = núcleos da CPU)
launch(Dispatchers.Default) { sortLargeCollection() }
// IO: I/O bloqueante (até 64 threads por padrão)
launch(Dispatchers.IO) { queryDatabase() }
// Customizado: para padrões específicos de I/O
val orderIoDispatcher = Executors.newFixedThreadPool(32)
.asCoroutineDispatcher()
// Aumentar tamanho do pool IO
System.setProperty("kotlinx.coroutines.io.parallelism", "128")
(2) Comparação de Ajuste de Dispatcher
| Cenário | Padrão | Após Ajuste | Melhoria |
|---|---|---|---|
| 64 consultas DB concorrentes | IO padrão 64 threads | Customizado 128 threads | 2x throughput |
| Computação CPU-intensiva | Default 4 núcleos | Fixo 8 threads | 2x throughput |
| Carga de trabalho mista | IO compartilhado | Pools de threads isolados | 50% redução de latência |
7. Benchmarking com JMH
(1) Configuração JMH
// build.gradle.kts
dependencies {
implementation("org.openjdk.jmh:jmh-core:1.37")
implementation("org.openjdk.jmh:jmh-generator-annprocess:1.37")
}
(2) Exemplo de Benchmark
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
open class OrderProcessingBenchmark {
private lateinit var orders: List<Order>
@Setup
fun setup() {
orders = (1..100_000).map {
Order("ORD-$it", it * 10.0, "CONFIRMED", "Customer-$it")
}
}
@Benchmark
fun listPipeline(): Map<String, List<Order>> {
return orders
.filter { it.total > 1_000 }
.map { it.copy(total = it.total * 0.9) }
.groupBy { it.customer }
}
@Benchmark
fun sequencePipeline(): Map<String, List<Order>> {
return orders.asSequence()
.filter { it.total > 1_000 }
.map { it.copy(total = it.total * 0.9) }
.groupBy { it.customer }
}
}
8. Exemplo Completo: Otimização de Throughput para Milhão de Pedidos
// ============================================
// OrderProcessor - Otimização de Performance
// Funcionalidade: Sequence, IntArray, Coroutines, Benchmarking
// ============================================
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
data class Order(val id: String, val total: Double, val status: String, val customer: String)
fun generateOrders(count: Int): List<Order> = (1..count).map {
Order("ORD-$it", it * 0.1, if (it % 5 == 0) "CANCELLED" else "CONFIRMED", "CUST-${it % 100}")
}
// Estratégia 1: List (eager)
fun processWithList(orders: List<Order>): Double {
return orders
.filter { it.status != "CANCELLED" }
.map { it.total }
.sum()
}
// Estratégia 2: Sequence (lazy)
fun processWithSequence(orders: List<Order>): Double {
return orders.asSequence()
.filter { it.status != "CANCELLED" }
.map { it.total }
.sum()
}
// Estratégia 3: IntArray (primitivo, sem boxing)
fun processWithIntArray(totals: DoubleArray): Double {
return totals.sum()
}
// Estratégia 4: Corrotinas (paralelo)
suspend fun processWithCoroutines(orders: List<Order>, chunkSize: Int = 10_000): Double {
return coroutineScope {
orders.chunked(chunkSize)
.map { chunk -> async(Dispatchers.Default) { chunk.filter { it.status != "CANCELLED" }.sumOf { it.total } } }
.awaitAll()
.sum()
}
}
fun main() = runBlocking {
val orderCount = 1_000_000
println("Gerando $orderCount pedidos...")
val orders = generateOrders(orderCount)
val totalsArray = DoubleArray(orderCount) { orders[it].total }
println("\n=== Benchmark de Performance ===\n")
// List
val listTime = measureTimeMillis { val r = processWithList(orders); println("List resultado: \$$r USD") }
println("List tempo: ${listTime}ms\n")
// Sequence
val seqTime = measureTimeMillis { val r = processWithSequence(orders); println("Sequence resultado: \$$r USD") }
println("Sequence tempo: ${seqTime}ms\n")
// IntArray
val arrayTime = measureTimeMillis { val r = processWithIntArray(totalsArray); println("DoubleArray resultado: \$$r USD") }
println("DoubleArray tempo: ${arrayTime}ms\n")
// Corrotinas
val coroTime = measureTimeMillis { val r = processWithCoroutines(orders); println("Coroutine resultado: \$$r USD") }
println("Coroutine tempo: ${coroTime}ms\n")
// Resumo
println("=== Resumo da Otimização ===")
println(" List vs Sequence speedup: ${"%.1f".format(listTime.toDouble() / seqTime)}x")
println(" List vs DoubleArray speedup: ${"%.1f".format(listTime.toDouble() / arrayTime)}x")
println(" List vs Coroutine speedup: ${"%.1f".format(listTime.toDouble() / coroTime)}x")
}
Saída (amostra; resultados reais dependem do hardware):
Gerando 1000000 pedidos...
=== Benchmark de Performance ===
List resultado: $4.99995E7 USD
List tempo: 120ms
Sequence resultado: $4.99995E7 USD
Sequence tempo: 45ms
DoubleArray resultado: $5.0E7 USD
DoubleArray tempo: 3ms
Coroutine resultado: $4.99995E7 USD
Coroutine tempo: 35ms
=== Resumo da Otimização ===
List vs Sequence speedup: 2.7x
List vs DoubleArray speedup: 40.0x
List vs Coroutine speedup: 3.4x
9. Exemplos práticos rápidos
▶ Exemplo: Primitivos vs Wrappers (boxing)
// Primitivos em arrays evitam boxing
fun sumList(): Double {
val list = List(1_000_000) { it.toDouble() }
return list.sum()
}
fun sumArray(): Double {
val array = DoubleArray(1_000_000) { it.toDouble() }
return array.sum()
}
val n = 5_000_000
val t1 = measureTimeMillis { repeat(100) { sumList() } }
val t2 = measureTimeMillis { repeat(100) { sumArray() } }
println("Lista boxed: ${t1}ms")
println("DoubleArray: ${t2}ms")
println("Speedup: ${t1.toDouble() / t2}")
▶ Exemplo: inline para reduzir overhead
// Função regular - lambda causa alocações
fun <T> processList(list: List<T>, action: (T) -> Unit) {
for (item in list) action(item)
}
// inline - sem alocações, ação inlined no caller
inline fun <T> processListInlined(list: List<T>, action: (T) -> Unit) {
for (item in list) action(item)
}
val items = listOf(1, 2, 3, 4, 5)
// Inline permite it
processListInlined(items) { println("Inline: $it") }
// Saída:
// Inline: 1
// Inline: 2
// ...
▶ Exemplo: Sequence vs List (cálculos preguiçosos)
fun expensiveOperation(n: Int): Int {
Thread.sleep(1)
return n * 2
}
// List - eager: processa todos os itens antes do take
val list = (1..100).map(::expensiveOperation)
val filtered = list.filter { it > 50 }.take(5)
// Sequence - lazy: para após take(5)
val seq = (1..100).asSequence()
.map(::expensiveOperation)
.filter { it > 50 }
.take(5)
.toList()
println("Primeiros 5: $seq")
println("Evitamos processar: ${100 - seq.size * 10} operações")
// Saída:
// Primeiros 5: [52, 54, 56, 58, 60]
// (Sequence só processa até encontrar os 5)
▶ Exemplo: Object allocation avoidance
// Singleton ao invés de criar objeto a cada chamada
class LogProcessor {
private val formatter = StringBuilder()
fun process(messages: List<String>): String {
formatter.setLength(0) // reusa buffer
messages.forEach { formatter.append(it).append('\n') }
return formatter.toString()
}
}
val processor = LogProcessor()
val logs = listOf("Pedido criado", "Pedido confirmado", "Pedido enviado")
println(processor.process(logs))
// Comparar com StringBuilder alocado por chamada
fun processNew(messages: List<String>): String {
val sb = StringBuilder() // novo toda vez
messages.forEach { sb.append(it).append('\n') }
return sb.toString()
}
println(processNew(logs))
▶ Exemplo: tailrec e redução de stack
// Sem tailrec - potencial StackOverflow
fun sumLoop(n: Long): Long {
return if (n == 0L) 0 else n + sumLoop(n - 1)
}
// Com tailrec - otimizado para loop
tailrec fun sumTailrec(n: Long, acc: Long = 0): Long {
return if (n == 0L) acc else sumTailrec(n - 1, acc + n)
}
val n = 100_000
// sumLoop(n) // StackOverflowError para n grande
val resultTailrec = sumTailrec(n)
println("Soma até $n: $resultTailrec")
// Saída: Soma até 100000: 5000050000
▶ Exemplo: String templates vs concat
// Não recomendado - muitas concatenações
fun buildOrderStringOld(id: String, total: Double, customer: String): String {
var result = ""
result += "ID: " + id
result += ", Total: $" + total
result += " USD, Cliente: " + customer
return result
}
// String template - mais rápido e legível
fun buildOrderStringNew(id: String, total: Double, customer: String) =
"ID: $id, Total: \$$total USD, Cliente: $customer"
// StringBuilder para múltiplas concatenações
fun buildLargeReport(rows: List<Pair<String, Double>>): String {
return buildString {
append("| ID | Total |\n")
rows.forEach { (id, total) ->
append("| $id | \$$total |\n")
}
}
}
println(buildOrderStringNew("ORD-001", 299.99, "Alice"))
val rows = listOf("ORD-001" to 299.99, "ORD-002" to 1500.00, "ORD-003" to 50.00)
println(buildLargeReport(rows))
▶ Exemplo: Benchmarking com JMH pattern
import kotlin.system.measureNanoTime
fun main() {
// Warmup
repeat(100_000) { operation1() }
val n = 1_000_000
val iterations = 1000
val t1 = measureNanoTime {
repeat(iterations) { operation1() }
}
val t2 = measureNanoTime {
repeat(iterations) { operation2() }
}
println("Op1: ${t1 / 1_000_000.0}ms total, ${t1 / (iterations * 1_000.0)} μs/op")
println("Op2: ${t2 / 1_000_000.0}ms total, ${t2 / (iterations * 1_000.0)} μs/op")
}
fun operation1(): Int = (1..100).sum()
fun operation2(): Int {
var total = 0
for (i in 1..100) total += i
return total
}
❓ Perguntas Frequentes
P: Quando devo otimizar a performance? R: Escreva código correto primeiro, meça os gargalos, depois otimize onde importa. Otimização prematura é a raiz de todos os males — mas escolher a estrutura de dados certa (Sequence vs List) não é otimização prematura; é uma decisão de design.
P: Inline aumenta o tamanho do APK/IPA? R: Sim. Cada chamada inlineada copia código para o local da chamada. Funções pequenas e frequentemente chamadas se beneficiam mais do inline. Inline de funções grandes causa inchaço. A biblioteca padrão já equilibra isso bem.
P: Sequence é sempre mais rápida que List? R: Nem sempre. Para dados pequenos (<1000) e operações de passo único, List pode ser mais rápida (cache-friendly, sem overhead de wrapper Sequence). Sequence brilha com 3+ operações em grandes conjuntos de dados.
P: Qual é mais rápido, IntArray ou
List<Int>? R: IntArray evita boxing e alocação de objetos — travessia e soma são 5-10x mais rápidas. Mas IntArray não suporta operadores funcionais (map/filter) — você precisará de loops manuais ou conversões.
P: Corrotinas são sempre mais rápidas que threads? R: Não. Para tarefas CPU-bound, corrotinas e threads têm performance similar. Corrotinas se destacam em cenários I/O-bound — lidando com I/O concorrente massivo com poucas threads, evitando overhead de bloqueio de threads.
P: Como medir corretamente a performance do Kotlin? R: Use JMH (Java Microbenchmark Harness). Não use
measureTimeMillispara micro-benchmarks — compilação JIT, GC e carregamento de classes distorcem os resultados. JMH cuida de tudo isso automaticamente.
📖 Resumo
inlineelimina alocação de objetos lambda, mas inline de funções grandes aumenta o tamanho do código- Avaliação preguiçosa com
Sequenceevita coleções intermediárias; preferida para dados grandes + operações em múltiplos passos IntArray/DoubleArrayevitam boxing; computação numérica 5-10x mais rápida- Ajuste de dispatcher de corrotinas: use
Dispatchers.IOpara I/O-bound,Dispatchers.Defaultpara CPU-bound - JMH é a ferramenta padrão para micro-benchmarking; evite medição manual
- Passos de otimização: correto → medir → otimizar → medir novamente
📝 Exercícios
- Iniciante (⭐): Use
measureTimeMillispara comparar o tempo de execução deListvsSequenceem 100.000 elementos. Dica:asSequence() - Intermediário (⭐⭐): Use
DoubleArraypara computar o valor total de pedidos e compare a performance comList<Double>. Dica:DoubleArray(size) { ... } - Avançado (⭐⭐⭐): Escreva um benchmark JMH formal comparando throughput de List/Sequence/Flow em 1 milhão de pedidos. Dica:
@Benchmark+@Setup