Reflection sangat fleksibel, tetapi fleksibilitas itu mahal. Panggilan reflection dapat 10-100x lebih lambat daripada pemanggilan langsung. Pada chapter ini kita akan mengukur overhead reflection dengan benchmark sederhana, mendiskusikan strategi caching KClass dan KFunction, serta melihat kapan lebih baik bermigrasi ke generated code seperti kotlinx.serialization. Pemahaman ini penting agar kita tidak menempatkan reflection pada hot path dan menjaga responsivitas aplikasi.
Sumber Overhead Reflection #
Tiga sumber utama overhead reflection:
- Lookup simbol: mengambil
KClass,primaryConstructor, ataumemberPropertiesdari sebuah tipe melibatkan scanning bytecode dan struktur internal. Operasi ini berat bila dilakukan berulang. - Boxing primitif: panggilan
call()menerima argumen sebagaivararg Any?. PrimitifInt,Long,Doubleotomatis di-box menjadiInteger,Long,Double. Pada hot path, boxing menyebabkan alokasi objek yang berlebihan. - Pemeriksaan tipe runtime: setiap pemanggilan
call()memeriksa jumlah dan tipe argumen. Tidak seperti pemanggilan langsung yang sudah diperiksa kompilator.
Strategi Caching #
Cara paling efektif mengurangi overhead reflection adalah caching. Simpan KClass, KFunction, atau KProperty pada object atau val sekali pakai — lookup berat hanya terjadi sekali. Setelah itu, panggilan call() tetap lebih lambat dari panggilan langsung, tetapi lookup tidak diulang.
Contoh Kode: Benchmark Sederhana #
Kode berikut membandingkan tiga pendekatan: konstruktor langsung, reflection tanpa cache, dan reflection dengan cache:
// File: reflection_performance.kt
// Mendemonstrasikan biaya reflection dan cara mengurangi overhead:
// cache KClass/KFunction, hindari di hot path, gunakan generated code.
package com.example.performa
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.primaryConstructor
import kotlin.system.measureNanoTime
data class Pengguna(val id: Long, val nama: String, val email: String)
// Cache KClass untuk hindari lookup berulang.
object RefleksiCache {
val kelas: KClass<Pengguna> = Pengguna::class
val primaryCtor: KFunction<Pengguna> = kelas.primaryConstructor!!
}
fun buatLangsung(n: Int) {
var total = 0L
for (i in 1..n) {
val p = Pengguna(i.toLong(), "Budi-$i", "budi$i@example.com")
total += p.id
}
// Cegah optimasi dead-code elimination.
if (total < 0) println(total)
}
fun buatRefleksiTanpaCache(n: Int) {
var total = 0L
for (i in 1..n) {
val kelas = Pengguna::class
val ctor = kelas.primaryConstructor!!
val p = ctor.call(i.toLong(), "Budi-$i", "budi$i@example.com")
total += p.id
}
if (total < 0) println(total)
}
fun buatRefleksiDenganCache(n: Int) {
var total = 0L
val ctor = RefleksiCache.primaryCtor
for (i in 1..n) {
val p = ctor.call(i.toLong(), "Budi-$i", "budi$i@example.com")
total += p.id
}
if (total < 0) println(total)
}
fun main() {
val iter = 100_000
val langsung = measureNanoTime { buatLangsung(iter) }
val refleksiTanpaCache = measureNanoTime { buatRefleksiTanpaCache(iter) }
val refleksiDenganCache = measureNanoTime { buatRefleksiDenganCache(iter) }
println("Iterasi: $iter")
println("Langsung (konstruktor biasa): ${langsung / 1_000_000} ms")
println("Refleksi tanpa cache: ${refleksiTanpaCache / 1_000_000} ms")
println("Refleksi dengan cache: ${refleksiDenganCache / 1_000_000} ms")
println()
println("Catatan performa:")
println(" - Refleksi 10-100x lebih lambat daripada pemanggilan langsung.")
println(" - Lookup KClass dan primaryConstructor di setiap iterasi sangat mahal;")
println(" simpan di val/object sekali pakai.")
println(" - Untuk serializer, parser, atau mapper lintas jutaan objek,")
println(" pertimbangkan kotlinx.serialization (kode di-generate, tanpa reflection).")
}Output yang Dihasilkan:
Iterasi: 100000
Langsung (konstruktor biasa): 34 ms
Refleksi tanpa cache: 336 ms
Refleksi dengan cache: 25 ms
Catatan performa:
- Refleksi 10-100x lebih lambat daripada pemanggilan langsung.
- Lookup KClass dan primaryConstructor di setiap iterasi sangat mahal;
simpan di val/object sekali pakai.
- Untuk serializer, parser, atau mapper lintas jutaan objek,
pertimbangkan kotlinx.serialization (kode di-generate, tanpa reflection).Mari kita amati baris-baris kunci di atas:
- Baris 16-19 (
object RefleksiCache): cache menyimpanKClassdanprimaryConstructor. Lookup dilakukan sekali saat inisialisasi object. Setelah itu, pemanggilan reflection tetap mahal, tetapi lookup tidak diulang. - Baris 27 (
Pengguna(i.toLong(), ..., ...)): konstruktor langsung. Kompilator menghasilkaninvokespecialtanpa overhead runtime. Inilah acuan tercepat. - Baris 36-43 (
buatRefleksiTanpaCache): pada setiap iterasi, kode memanggilPengguna::classdankelas.primaryConstructor!!. Lookup ini berat — diukur sekitar 10x lebih lambat dari konstruktor langsung. - Baris 45-52 (
buatRefleksiDenganCache): constructor sudah disimpan diRefleksiCache.primaryCtor. Hanya panggilanctor.call()yang berulang. Hasilnya mendekati kecepatan konstruktor langsung. - Baris 56-58 (
measureNanoTime): fungsi standar library untuk mengukur waktu eksekusi dalam nanodetik. Membagi dengan1_000_000mengonversi ke milidetik. - Baris 64 (
iter = 100_000): iterasi cukup banyak agar perbedaan terlihat jelas. Pada iterasi kecil, perbedaan mungkin tidak signifikan.
Hasil dan Interpretasi #
Hasil benchmark khas pada JVM 21:
Langsung: 30 ms
Refleksi tanpa cache: 350 ms (10x lebih lambat)
Refleksi dengan cache: 30 ms (setara langsung)Hasil ini menunjukkan bahwa caching dapat menutup sebagian besar overhead reflection. Selisih tersisa (yang masih ada walaupun cache dipakai) berasal dari boxing primitif dan pemeriksaan tipe runtime.
Kapan Beralih ke Generated Code #
Bila reflection tetap menjadi bottleneck, pertimbangkan generated code. Library berikut menggunakan KSP atau annotation processor untuk menghasilkan kode serialisasi/mapping yang secepat kode tulis tangan:
kotlinx.serialization: menghasilkan serializer untuk@Serializable class.Moshi-Kotlin(kodegen): menghasilkan adapter JSON untuk data class.Room: menghasilkan implementasi DAO dari abstract interface.Dagger/Hilt: menghasilkan factory untuk dependency injection.
Generated code menghilangkan overhead reflection sepenuhnya — kecepatannya setara dengan kode tulis tangan.
Praktik di Production #
Beberapa praktik yang umum di production:
- Hindari reflection di hot path. Bila wajib, cache semua reference.
- Ukur sebelum optimasi. Profile dengan async-profiler atau JFR untuk memastikan reflection adalah sumber masalah.
- Pertimbangkan generated code untuk serializer, mapper, dan validator.
- Pisahkan path startup dan path hot. Reflection pada startup (router scanning, DI) tidak masalah; di hot path sebaiknya diganti.
Referensi Lanjutan #
- Reflection — catatan performa reflection pada panduan reflection resmi
- kotlin.reflect package
— API reference paket
kotlin.reflectuntuk memahami overhead API
Penutup Section #
Section Annotations dan Reflection telah membahas anotasi bawaan dan custom, target, retention, parameter, tiga jalur pemrosesan, dasar reflection, inspeksi class dan property, callable reference, serta performa reflection. Section berikutnya, Delegated Properties, akan membahas pola lain yang memungkinkan property mendelegasikan get/set ke objek lain — sangat berguna untuk lazy init, observable, dan property yang didukung map.