Lambda di Kotlin dikompilasi menjadi instance Function1, Function2, dan seterusnya. Setiap pemanggilan function yang menerima lambda mengalokasikan instance ini di heap — beban yang tampak kecil tetapi menumpuk pada loop ketat. Modifier inline mengatasi masalah ini dengan menyisipkan body function beserta lambda langsung ke call-site, menghapus instance FunctionN sepenuhnya. Pada chapter ini kita akan membedah mekanisme inline, mendemokan penghematan alokasi, serta membahas trade-off utama: bytecode yang lebih besar karena setiap call-site mendapat salinan body.
Mekanisme inline Function #
Tanpa inline, kompilator menghasilkan bytecode yang memanggil function dan mengoper instance Function1. Dengan inline, body function disalin langsung ke tempat pemanggilan, dan body lambda juga disalin ke titik pemanggilan lambda. Hasilnya: tidak ada instance Function1 yang dibuat.
fun repeatNonInline(times: Int, action: (Int) -> Unit) {
for (i in 0 until times) action(i) // action = instance Function1
}
inline fun repeatInline(times: Int, action: (Int) -> Unit) {
for (i in 0 until times) action(i) // action disalin ke call-site
}Saat repeatInline(5) { println(it) } dipanggil, kompilator menyisipkan loop for dan println langsung ke pemanggil — tidak ada function call, tidak ada instance Function1. Standard library Kotlin memakai inline ekstensif (forEach, map, filter, let, run, apply) sehingga kode fungsional Kotlin seringkali secepat kode imperatif.
Demo Step-Counting #
Cuplikan berikut membandingkan alokasi lambda antara function non-inline dan inline. Untuk keperluan demo, kita menambahkan counter yang mensimulasikan alokasi instance Function1:
// inline_functions_performance.kt — Demo inline function dan overhead lambda
// === 1. Non-inline function dengan lambda ===
// Setiap pemanggilan mengalokasasi Runnable atau Function1 baru
fun repeatNonInline(times: Int, action: (Int) -> Unit) {
for (i in 0 until times) action(i)
}
// === 2. Inline function ===
// Lambda dipanggil langsung di call-site; tidak ada instance Function1
inline fun repeatInline(times: Int, action: (Int) -> Unit) {
for (i in 0 until times) action(i)
}
// === 3. Inline dengan reified type ===
inline fun <reified T> filterByType(items: List<Any>): List<T> {
val result = mutableListOf<T>()
for (item in items) {
if (item is T) result.add(item)
}
return result
}
// === 4. Crossinline: lambda tidak boleh return non-local ===
inline fun runInCriticalSection(crossinline block: () -> Unit) {
// Simulasi: kunci lalu lepas
lock()
try {
block()
} finally {
unlock()
}
}
fun lock() { /* no-op untuk demo */ }
fun unlock() { /* no-op untuk demo */ }
// === 5. Noinline: parameter lambda tetap dialokasikan ===
inline fun processAll(noinline debug: () -> Unit, action: (Int) -> Unit) {
debug() // tidak di-inline, tetap Function0
for (i in 1..3) action(i)
}
// === 6. Performance comparison (step counting) ===
class CallCounter {
var lambdaAllocations: Int = 0
var totalCalls: Int = 0
fun alloc() { lambdaAllocations++ }
fun call() { totalCalls++ }
fun report(label: String): String =
" %-30s lambda allocs=%-5d calls=%-7d".format(label, lambdaAllocations, totalCalls)
fun reset() { lambdaAllocations = 0; totalCalls = 0 }
}
fun main() {
println("=== inline Functions Performance ===")
println()
println("Membandingkan overhead lambda pada function non-inline vs inline.")
println()
val counter = CallCounter()
// === 1. Non-inline: setiap call = 1 lambda instance ===
println("=== 1. Non-inline (lambda dialokasikan) ===")
counter.reset()
for (run in 1..3) {
counter.alloc() // simulasi alokasi lambda
repeatNonInline(5) { counter.call() }
}
println(counter.report("3 run x 5 calls = 15"))
println(" -> Setiap iterasi loop luar membuat 1 instance Function1 baru")
println()
// === 2. Inline: tidak ada alokasi lambda ===
println("=== 2. Inline (tidak ada alokasi) ===")
counter.reset()
for (run in 1..3) {
// Tidak ada alloc() karena inline
repeatInline(5) { counter.call() }
}
println(counter.report("3 run x 5 calls = 15"))
println(" -> Lambda disalin langsung ke call-site; nol instance Function1")
println()
// === 3. Reified type ===
println("=== 3. Reified Type ===")
val campur: List<Any> = listOf("Budi", 42, "Siti", 99L, "Dewi", 7)
val strings = filterByType<String>(campur)
println(" Input: $campur")
println(" String saja: $strings")
println(" -> filterByType<String> bisa cek T is String tanpa Class<T>")
println()
// === 4. Crossinline ===
println("=== 4. Crossinline ===")
runInCriticalSection {
println(" Di dalam critical section")
}
println(" -> Lambda inline, tidak bisa return non-local dari sini")
println()
// === 5. Noinline ===
println("=== 5. Noinline ===")
processAll(
debug = { println(" [DEBUG] mulai proses") },
action = { i -> println(" proses item $i") }
)
println()
// === 6. Aturan inline ===
println("=== 6. Aturan inline ===")
val rules = listOf(
"1. inline function dengan lambda kecil -> hemat alokasi",
"2. inline function dipanggil sering (loop) -> efek maksimal",
"3. Hindari inline untuk function besar -> bytecode membengkak",
"4. reified type butuh inline (T is Something)",
"5. crossinline: lambda dengan return non-local dilarang",
"6. noinline: parameter tertentu tetap dialokasikan"
)
rules.forEach { println(" " + it) }
println()
// === 7. Trade-off ringkasan ===
println("=== 7. Ringkasan Trade-off ===")
val tradeoffs = listOf(
Triple("Alokasi lambda", "Tetap (1 per call)", "0 (disalin ke call-site)"),
Triple("Call overhead", "Function call + closure", "Langsung, no closure"),
Triple("Bytecode size", "Kecil (satu copy)", "Besar (salin tiap call-site)"),
Triple("JIT inlining", "Mungkin setelah warmup", "Sudah inline saat compile"),
Triple("Cocok untuk", "Function besar, jarang dipanggil", "Function kecil dengan lambda")
)
tradeoffs.forEach { (aspek, nonInline, inlineRow) ->
println(" %-20s | non-inline: %-30s | inline: $inlineRow".format(aspek, nonInline))
}
}Output yang Dihasilkan:
=== inline Functions Performance ===
Membandingkan overhead lambda pada function non-inline vs inline.
=== 1. Non-inline (lambda dialokasikan) ===
3 run x 5 calls = 15 lambda allocs=3 calls=15
-> Setiap iterasi loop luar membuat 1 instance Function1 baru
=== 2. Inline (tidak ada alokasi) ===
3 run x 5 calls = 15 lambda allocs=0 calls=15
-> Lambda disalin langsung ke call-site; nol instance Function1
=== 3. Reified Type ===
Input: [Budi, 42, Siti, 99, Dewi, 7]
String saja: [Budi, Siti, Dewi]
-> filterByType<String> bisa cek T is String tanpa Class<T>
=== 4. Crossinline ===
Di dalam critical section
-> Lambda inline, tidak bisa return non-local dari sini
=== 5. Noinline ===
[DEBUG] mulai proses
proses item 1
proses item 2
proses item 3
=== 6. Aturan inline ===
1. inline function dengan lambda kecil -> hemat alokasi
2. inline function dipanggil sering (loop) -> efek maksimal
3. Hindari inline untuk function besar -> bytecode membengkak
4. reified type butuh inline (T is Something)
5. crossinline: lambda dengan return non-local dilarang
6. noinline: parameter tertentu tetap dialokasikan
=== 7. Ringkasan Trade-off ===
Alokasi lambda | non-inline: Tetap (1 per call) | inline: 0 (disalin ke call-site)
Call overhead | non-inline: Function call + closure | inline: Langsung, no closure
Bytecode size | non-inline: Kecil (satu copy) | inline: Besar (salin tiap call-site)
JIT inlining | non-inline: Mungkin setelah warmup | inline: Sudah inline saat compile
Cocok untuk | non-inline: Function besar, jarang dipanggil | inline: Function kecil dengan lambdaMari kita bedah hasil pengukuran di atas:
- Non-inline: 3 run menghasilkan 3 alokasi
Function1(satu per iterasi loop luar). Total call adalah 15 (3 × 5), dan setiap call di dalamrepeatNonInlineadalah method call pada instanceFunction1. - Inline: 0 alokasi
Function1. Lambdacounter.call()disalin langsung ke call-site; loopfordirepeatInlinedisalin ke loop luar. Total call tetap 15, tetapi tidak ada instance baru. - Reified:
filterByType<String>(campur)bekerja karenainlinemengaktifkanreified— kompilator tahuT = Stringdi call-site sehinggaitem is Tmenjadiitem is String. Tanpa inline, kita harus mengoperClass<T>eksplisit. - Crossinline: lambda dipanggil dalam konteks non-local (di dalam
try-finally).crossinlinemencegah lambda melakukanreturnnon-local yang akan melewatifinally. Tanpa modifier ini, kompilator menolak. - Noinline: parameter
debugditandainoinlinesehingga tetap menjadi instanceFunction0. Berguna bila kita ingin menyimpan lambda untuk dipakai nanti (mis. callback), bukan langsung dieksekusi.
Reified Type Parameter #
reified hanya bekerja pada inline function. Kompilator mengganti T dengan tipe konkret di setiap call-site, sehingga item is T, T::class.java, dan Array<T> tersedia. Tanpa reified, type erasure menghapus T di runtime dan kita harus mengoper Class<T> manual.
inline fun <reified T> List<*>.filterByType(): List<T> =
filter { it is T } as List<T>
val strings: List<String> = listOf("a", 1, "b", 2).filterByType<String>()
// kompilator tahu T = String di call-siteTrade-off Bytecode Size #
inline menyimpan body function ke setiap call-site. Untuk function kecil yang dipanggil di banyak tempat, bytecode total bisa membengkak 2-3x. Standard library Kotlin mengandalkan JIT untuk inline function kecil; untuk function besar, pertimbangkan tidak memakai inline.
Referensi Lanjutan #
- Inline functions
— panduan resmi fungsi
inlineKotlin beserta dampaknya pada bytecode dan performa
Selanjutnya #
Penghematan alokasi dan inline hanyalah satu sisi performance. Bab berikutnya membahas performance regression testing — cara memastikan perubahan kode tidak menurunkan performance dari waktu ke waktu melalui benchmark CI.