Kotlin Performance Optimization Explained
Performance isn't premature optimization — it's making the right choices: Charlie replaces List chains with Sequence to avoid intermediate collection allocations for millions of orders, uses IntArray instead of Array<Int> to eliminate boxing, and validates every optimization with JMH.
1. What You'll Learn
inlinefunctions: eliminate lambda object allocation- Collection selection:
ListvsSequencevsFlowvsArray - Coroutine dispatcher tuning
- Primitive type arrays:
IntArrayvsArray<Int> - Charlie's hands-on: million-order throughput optimization
2. An Architect's Real Story
(1) Pain Point: Out of Memory Processing a Million Orders
Charlie's OrderProcessor created a new collection at every step when processing a million orders — peak memory hit 4GB, GC pauses reached 2 seconds.
(2) Performance Optimization Solution
KOTLIN
// Before: 3 intermediate collections, 4GB peak memory
orders.map { enrich(it) }.filter { it.total > 100 }.groupBy { it.region }
// After: 0 intermediate collections, 200MB peak memory
orders.asSequence()
.map { enrich(it) }
.filter { it.total > 100 }
.groupBy { it.region } // Only 1 final collection
Sequence lazy evaluation + primitive type arrays + coroutine pool tuning = 95% memory reduction, 10x throughput improvement.
3. Inline Functions Eliminate Overhead
(1) The Hidden Cost of Lambdas
KOTLIN
// Non-inline: Lambda compiles to anonymous class
fun process(order: Order, block: (Order) -> String): String {
return block(order) // Creates Function1 object every call
}
// Inline: Lambda code is inlined at call site
inline fun process(order: Order, block: (Order) -> String): String {
return block(order) // No object allocation!
}
(2) Measuring the Inline Effect
KOTLIN
// JMH micro-benchmark (conceptual)
// Non-inline: ~50ns per call (object allocation)
// Inline: ~5ns per call (no allocation, JIT inlines further)
(3) Inline Usage Guidelines
| Scenario | Recommendation | Reason |
|---|---|---|
| Higher-order function (1-5 line body) | ✅ inline | Eliminates lambda object allocation |
| reified type parameter | ✅ must inline | Preserve type at compile time |
| Large function body (>20 lines) | ❌ don't inline | Increases code size |
| Lambda stored/passed around | ❌ don't inline | Inlined lambda no longer exists |
4. Collection Selection
(1) Four Data Processing Approaches
KOTLIN
// 1. List (Eager): each step creates new collection
val result1 = orders
.map { enrich(it) } // New List
.filter { it.total > 100 } // New List
.toList() // New List
// 2. Sequence (Lazy): processes one element at a time
val result2 = orders.asSequence()
.map { enrich(it) }
.filter { it.total > 100 }
.toList() // Only 1 List
// 3. Flow (Async Lazy): suspend-capable lazy stream
val result3 = orders.asFlow()
.map { enrichAsync(it) } // Can be suspend
.filter { it.total > 100 }
.toList() // Only 1 List
// 4. Array: lowest overhead, fixed size
val result4 = ordersArray
.map { enrich(it) } // Creates new Array
.filter { it.total > 100 } // No filter on Array
(2) Collection Selection Decision Tree
flowchart LR
A[Data Processing] --> B{Async needed?}
B -->|Yes| C[Flow]
B -->|No| D{Data size?}
D -->|Large >10K| E{Multiple steps?}
D -->|Small <10K| F[List]
E -->|Yes| G[Sequence]
E -->|No| F
A --> H{Primitive types?}
H -->|Yes| I[IntArray/DoubleArray]
H -->|No| A
(3) Performance Comparison
| Approach | Memory | CPU | Best For |
|---|---|---|---|
List |
O(n×steps) | Low (cache-friendly) | Small data, few steps |
Sequence |
O(1) | Medium (full pipeline per element) | Large data, many steps |
Flow |
O(1) | Medium + async overhead | Async data sources |
Array |
O(n) | Lowest (no boxing) | Primitives, fixed size |
5. Primitive Type Arrays
(1) IntArray vs Array<Int>
KOTLIN
// Array<Int>: each element is boxed Integer object
val boxed: Array<Int> = Array(1_000_000) { it }
// Memory: ~24MB (4MB data + 20MB object headers)
// IntArray: primitive int[], no boxing
val primitive: IntArray = IntArray(1_000_000) { it }
// Memory: ~4MB (no object overhead)
// 6x memory savings for primitive arrays!
(2) Primitive Array Selection
| Type | Boxed Array | Primitive Array | Savings |
|---|---|---|---|
| Int | Array<Int> |
IntArray |
~6x |
| Long | Array<Long> |
LongArray |
~6x |
| Double | Array<Double> |
DoubleArray |
~6x |
| Boolean | Array<Boolean> |
BooleanArray |
~8x |
6. Coroutine Dispatcher Tuning
(1) Dispatcher Selection
KOTLIN
// Default: CPU-bound (parallelism = CPU cores)
launch(Dispatchers.Default) { sortLargeCollection() }
// IO: blocking I/O (up to 64 threads by default)
launch(Dispatchers.IO) { queryDatabase() }
// Custom: for specific I/O patterns
val orderIoDispatcher = Executors.newFixedThreadPool(32)
.asCoroutineDispatcher()
// Increase IO pool size
System.setProperty("kotlinx.coroutines.io.parallelism", "128")
(2) Dispatcher Tuning Comparison
| Scenario | Default | After Tuning | Improvement |
|---|---|---|---|
| 64 concurrent DB queries | IO default 64 threads | Custom 128 threads | 2x throughput |
| CPU-intensive computation | Default 4 cores | Fixed 8 threads | 2x throughput |
| Mixed workload | Shared IO | Isolated thread pools | 50% latency reduction |
7. JMH Benchmarking
(1) JMH Configuration
KOTLIN
// build.gradle.kts
dependencies {
implementation("org.openjdk.jmh:jmh-core:1.37")
implementation("org.openjdk.jmh:jmh-generator-annprocess:1.37")
}
(2) Benchmark Example
KOTLIN
@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. Complete Example: Million-Order Throughput Optimization
KOTLIN
// ============================================
// OrderProcessor - Performance Optimization
// Feature: 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}")
}
// Strategy 1: List (eager)
fun processWithList(orders: List<Order>): Double {
return orders
.filter { it.status != "CANCELLED" }
.map { it.total }
.sum()
}
// Strategy 2: Sequence (lazy)
fun processWithSequence(orders: List<Order>): Double {
return orders.asSequence()
.filter { it.status != "CANCELLED" }
.map { it.total }
.sum()
}
// Strategy 3: IntArray (primitive, no boxing)
fun processWithIntArray(totals: DoubleArray): Double {
return totals.sum()
}
// Strategy 4: Coroutines (parallel)
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("Generating $orderCount orders...")
val orders = generateOrders(orderCount)
val totalsArray = DoubleArray(orderCount) { orders[it].total }
println("\n=== Performance Benchmark ===\n")
// List
val listTime = measureTimeMillis { val r = processWithList(orders); println("List result: \$$r USD") }
println("List time: ${listTime}ms\n")
// Sequence
val seqTime = measureTimeMillis { val r = processWithSequence(orders); println("Sequence result: \$$r USD") }
println("Sequence time: ${seqTime}ms\n")
// IntArray
val arrayTime = measureTimeMillis { val r = processWithIntArray(totalsArray); println("DoubleArray result: \$$r USD") }
println("DoubleArray time: ${arrayTime}ms\n")
// Coroutines
val coroTime = measureTimeMillis { val r = processWithCoroutines(orders); println("Coroutine result: \$$r USD") }
println("Coroutine time: ${coroTime}ms\n")
// Summary
println("=== Optimization Summary ===")
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")
}
Output (sample; actual results depend on hardware):
TEXT
Generating 1000000 orders...
=== Performance Benchmark ===
List result: $4.99995E7 USD
List time: 120ms
Sequence result: $4.99995E7 USD
Sequence time: 45ms
DoubleArray result: $5.0E7 USD
DoubleArray time: 3ms
Coroutine result: $4.99995E7 USD
Coroutine time: 35ms
=== Optimization Summary ===
List vs Sequence speedup: 2.7x
List vs DoubleArray speedup: 40.0x
List vs Coroutine speedup: 3.4x
❓ FAQ
Q When should I optimize performance?
A Write correct code first, measure bottlenecks, then optimize where it matters. Premature optimization is the root of all evil — but choosing the right data structure (Sequence vs List) isn't premature optimization; it's a design decision.
Q Does inline increase APK/IPA size?
A Yes. Every inlined call copies code to the call site. Small, frequently-called functions benefit most from inlining. Large function inlining causes bloating. The standard library already balances this well.
Q Is Sequence always faster than List?
A Not always. For small data (<1000) and single-step operations, List may be faster (cache-friendly, no Sequence wrapper overhead). Sequence shines with 3+ step operations on large datasets.
Q Which is faster, IntArray or
List<Int>?A IntArray avoids boxing and object allocation — traversal and summation are 5-10x faster. But IntArray doesn't support functional operators (map/filter) — you'll need manual loops or conversions.
Q Are coroutines always faster than threads?
A No. For CPU-bound tasks, coroutines and threads perform similarly. Coroutines excel in I/O-bound scenarios — handling massive concurrent I/O with few threads, avoiding thread blocking overhead.
Q How do I properly measure Kotlin performance?
A Use JMH (Java Microbenchmark Harness). Don't use
measureTimeMillis for micro-benchmarks — JIT compilation, GC, and class loading all skew results. JMH handles all of these automatically.📖 Summary
inlineeliminates lambda object allocation, but inlining large functions increases code sizeSequencelazy evaluation avoids intermediate collections; preferred for large data + multi-step operationsIntArray/DoubleArrayavoid boxing; numerical computation 5-10x faster- Coroutine dispatcher tuning: use
Dispatchers.IOfor I/O-bound,Dispatchers.Defaultfor CPU-bound - JMH is the standard tool for micro-benchmarking; avoid manual measurement
- Optimization steps: correct → measure → optimize → measure again
📝 Exercises
- Beginner (⭐): Use
measureTimeMillisto compareListvsSequenceexecution time on 100,000 elements. Hint:asSequence() - Intermediate (⭐⭐): Use
DoubleArrayto compute total order value and compare performance againstList<Double>. Hint:DoubleArray(size) { ... } - Advanced (⭐⭐⭐): Write a formal JMH benchmark comparing List/Sequence/Flow throughput on 1 million orders. Hint:
@Benchmark+@Setup



