Microbenchmark adalah pengukuran kinerja function atau potongan kode kecil. Sederhana kelihatannya, namun banyak jebakan menanti developer yang baru pertama kali mencoba. JIT compiler dapat menghapus kode yang hasilnya tidak dipakai, GC dapat memunculkan jeda yang mengganggu pengukuran, dan cold start berbeda jauh dari steady state. Pada bab ini kita akan membedah tiga jebakan utama microbenchmark, mendemokan setiap jebakan dengan step-counting deterministik, dan menyusun aturan emas yang menjamin hasil benchmark dapat dipercaya.
Tiga Jebakan Utama #
Setiap microbenchmark rentan terhadap tiga jebakan yang membuat hasilnya tidak valid. Jebakan pertama adalah JIT warmup: JIT compiler butuh waktu untuk mengidentifikasi hot code dan mengoptimasinya. Pengukuran sebelum warmup akan terlalu pesimistis. Jebakan kedua adalah dead code elimination (DCE): compiler dapat menghapus seluruh kode yang hasilnya tidak dipakai, sehingga benchmark mengukur “apa-apa”. Jebakan ketiga adalah GC pauses: alokasi objek dalam loop benchmark memicu GC yang menambahkan jeda tak terduga.
| Jebakan | Penyebab | Akibat | Solusi |
|---|---|---|---|
| JIT warmup | JIT butuh ~10_000 panggilan | Hasil awal pesimistis | Warmup sebelum ukur |
| Dead code elimination | Hasil tidak dipakai | Benchmark mengukur 0 ops | Pakai sink variable |
| GC pauses | Alokasi berlebih | Variance tinggi | Kurangi alokasi |
Jebakan 1: JIT Warmup #
JVM memulai dalam mode interpreter (slow) dan mengaktifkan JIT compilation setelah function dianggap “hot” (sekitar 10_000 panggilan). Tanpa warmup, benchmark mengukur performance interpreter, bukan performance production. Solusinya: jalankan kode beberapa ribu kali sebelum mulai mengukur.
fun benchmark() {
// Warmup: 10_000 panggilan tanpa pengukuran
repeat(10_000) { cpuIntensive(it) }
// Pengukuran: 1_000 panggilan yang diukur
repeat(1_000) { cpuIntensive(it) }
}Setelah warmup, JIT telah mengoptimasi function. Pengukuran berikutnya akan menunjukkan performance steady state yang merupakan performance yang akan dialami pengguna di production.
Jebakan 2: Dead Code Elimination #
Compiler optimasi (terutama JIT dan GraalVM) dapat menghapus kode yang hasilnya tidak pernah dipakai. Bila benchmark memanggil cpuIntensive(x) tetapi return value-nya diabaikan, compiler mungkin menghapus seluruh loop tersebut. Akibatnya, benchmark melaporkan “sangat cepat” padahal tidak ada pekerjaan yang dilakukan.
var sink: Int = 0 // variable yang menyimpan hasil agar tidak di-eliminasi
repeat(1_000) {
sink += cpuIntensive(it) // hasil ditambahkan ke sink
}
println(sink) // pakai sink agar tidak dianggap dead codeBlackhole di JMH adalah implementasi resmi dari pola ini. Blackhole.consume(x) memberi sinyal ke compiler bahwa x dipakai, sehingga tidak boleh dieliminasi.
Jebakan 3: GC Pauses #
Bila benchmark mengalokasikan objek di setiap iterasi, GC akan dipicu untuk membersihkan objek-objek tersebut. GC pause menyebabkan sebagian iterasi menjadi sangat lambat, sementara iterasi lain normal. Akibatnya, variance hasil benchmark menjadi tinggi dan rata-rata tidak representatif.
fun benchmarkBuruk() {
repeat(1_000) {
val list = ArrayList<String>(100) // alokasi besar per iterasi
repeat(100) { list.add("item-$it") }
// list tidak dipakai lagi, menunggu GC
}
}Solusi: kurangi alokasi dalam loop utama. Reuse object, pakai primitive array daripada boxed collection, atau pindahkan alokasi ke luar loop bila memungkinkan.
Demo Tiga Jebakan #
Cuplikan berikut mendemokan ketiga jebakan dengan output deterministik. JIT warmup dihitung berdasarkan threshold khas, DCE dijelaskan via sink variable, dan GC pause disimulasikan setiap 25 iterasi:
// Demo jebakan microbenchmark: JIT warmup, dead code elimination,
// GC pauses. Menggunakan step counter (bukan nanotime) agar deterministik.
// Simulasi "warmup": hitung berapa kali function dipanggil sebelum
// JIT dianggap cukup mengoptimasi (threshold khas: 10_000 panggilan).
class BenchmarkState {
var invocationCount: Long = 0
var jitCompiled: Boolean = false
var jitThreshold: Long = 10_000
var deadCodeEliminated: Int = 0
var gcPaused: Boolean = false
}
fun cpuIntensive(x: Int): Int {
// Operasi yang dapat di-eliminasi compiler bila hasil tidak dipakai
var acc = 0
for (i in 0 until 100) {
acc += x * i
}
return acc
}
fun cpuIntensiveUnused(x: Int): Int {
// Sama seperti di atas, namun pemanggil tidak memakai return value
var acc = 0
for (i in 0 until 100) {
acc += x * i
}
return acc
}
fun main() {
println("=== Benchmarking Fundamentals ===")
println()
println("Tiga jebakan umum microbenchmark:")
println(" 1. JIT warmup (cold start vs steady state)")
println(" 2. Dead code elimination (DCE)")
println(" 3. GC pauses (karena alokasi berlebih)")
println()
// === Jebakan 1: JIT Warmup ===
println("=== Jebakan 1: JIT Warmup ===")
val state = BenchmarkState()
val samples = mutableListOf<Pair<Int, Long>>()
repeat(3) { phase ->
state.invocationCount = 0
repeat(15_000) {
cpuIntensive(it)
state.invocationCount++
if (!state.jitCompiled && state.invocationCount >= state.jitThreshold) {
state.jitCompiled = true
}
}
val avg = 15_000L
samples.add(phase + 1 to avg)
println(" Phase ${phase + 1}: 15000 panggilan, JIT compiled=${state.jitCompiled}")
}
println(" Pelajaran: 3 phase pertama mengaktifkan JIT; phase awal lebih lambat.")
println()
// === Jebakan 2: Dead Code Elimination ===
println("=== Jebakan 2: Dead Code Elimination ===")
var sink1: Int = 0
var sink2: Int = 0
// Variants A: return value dipakai (tidak di-eliminasi)
state.invocationCount = 0
repeat(100) { sink1 += cpuIntensive(it) }
// Variants B: return value TIDAK dipakai (compiler mungkin eliminasi)
state.deadCodeEliminated = 0
repeat(100) { cpuIntensiveUnused(it) } // tidak ada yang memakai return
println(" Variant A (hasil dipakai): 100 panggilan efektif")
println(" Variant B (hasil tidak dipakai): compiler mungkin eliminasi seluruh loop")
println(" Pelajaran: selalu 'consume' hasil benchmark dengan sink variable.")
println(" sink1=$sink1, sink2=$sink2 (mencegah compiler optimasi variant A)")
println()
// === Jebakan 3: GC Pauses ===
println("=== Jebakan 3: GC Pauses ===")
println(" Skenario: 1000 alokasi String per iterasi, 100 iterasi")
var totalAlokasi = 0
var totalIterations = 0
repeat(100) { i ->
val list = ArrayList<String>(10)
repeat(10) { list.add("iterasi-$i-item-$it") }
totalAlokasi += list.size
totalIterations++
// Simulasi: setiap 25 iterasi, GC pause terjadi
if (i in listOf(25, 50, 75)) {
state.gcPaused = true
println(" [GC] minor pause di iterasi $i")
}
}
println(" Total alokasi: $totalAlokasi, iterasi: $totalIterations")
println(" GC pause terjadi: ${if (state.gcPaused) "ya" else "tidak"}")
println(" Pelajaran: alokasi berlebih menyebabkan GC, yang menambah variance benchmark.")
println()
// === Aturan Emas ===
println("=== Aturan Emas Microbenchmark ===")
val rules = listOf(
"1. Selalu warmup (10_000+ panggilan) sebelum mengukur",
"2. Konsumsi hasil dengan sink variable untuk cegah DCE",
"3. Kurangi alokasi di dalam loop utama",
"4. Jalankan beberapa iterasi, ambil median bukan rata-rata",
"5. Gunakan framework benchmark (JMH) yang menangani jebakan otomatis"
)
rules.forEach { println(" $it") }
}Output yang Dihasilkan:
=== Benchmarking Fundamentals ===
Tiga jebakan umum microbenchmark:
1. JIT warmup (cold start vs steady state)
2. Dead code elimination (DCE)
3. GC pauses (karena alokasi berlebih)
=== Jebakan 1: JIT Warmup ===
Phase 1: 15000 panggilan, JIT compiled=true
Phase 2: 15000 panggilan, JIT compiled=true
Phase 3: 15000 panggilan, JIT compiled=true
Pelajaran: 3 phase pertama mengaktifkan JIT; phase awal lebih lambat.
=== Jebakan 2: Dead Code Elimination ===
Variant A (hasil dipakai): 100 panggilan efektif
Variant B (hasil tidak dipakai): compiler mungkin eliminasi seluruh loop
Pelajaran: selalu 'consume' hasil benchmark dengan sink variable.
sink1=24502500, sink2=0 (mencegah compiler optimasi variant A)
=== Jebakan 3: GC Pauses ===
Skenario: 1000 alokasi String per iterasi, 100 iterasi
[GC] minor pause di iterasi 25
[GC] minor pause di iterasi 50
[GC] minor pause di iterasi 75
Total alokasi: 1000, iterasi: 100
GC pause terjadi: ya
Pelajaran: alokasi berlebih menyebabkan GC, yang menambah variance benchmark.
=== Aturan Emas Microbenchmark ===
1. Selalu warmup (10_000+ panggilan) sebelum mengukur
2. Konsumsi hasil dengan sink variable untuk cegah DCE
3. Kurangi alokasi di dalam loop utama
4. Jalankan beberapa iterasi, ambil median bukan rata-rata
5. Gunakan framework benchmark (JMH) yang menangani jebakan otomatisMari kita bedah bagian-bagian kunci dari kode di atas:
BenchmarkState: class yang melacak status benchmark (apakah JIT sudah aktif, jumlah invocation, GC pause). Pada JMH nyata,@Stateannotation berperan sama.jitThreshold = 10_000: angka khas di HotSpot JVM. Setelah function dipanggil 10_000 kali, JIT menganggapnya “hot” dan mengaktifkan compilation ke native code. Pada demo,state.jitCompileddi-settruesaat threshold terlewati.cpuIntensiveUnusedvscpuIntensive: kedua function identik, namun pemanggilancpuIntensiveUnusedtidak menggunakan return value. Compiler dapat mengenali pola ini dan menghapus seluruh loop, sehingga “benchmark” variant B sebenarnya mengukur 0 operasi.sink1 += cpuIntensive(it): pola sink variable. Hasil setiap pemanggilan ditambahkan kesink1, sehingga compiler tidak bisa mengeliminasi. Pada akhir,sink1di-print untuk memastikan tidak dianggap dead code.- Simulasi GC pause di iterasi 25/50/75: setiap 25 iterasi, demo mencetak pesan “[GC] minor pause”. Pada benchmark nyata, GC pause terdeteksi via JFR atau
-verbose:gc.
Aturan Emas Microbenchmark #
Aturan emas yang muncul dari tiga jebakan di atas adalah: warmup dulu, konsumsi hasil, kurangi alokasi, ambil median, dan gunakan framework. Aturan kelima paling penting: JMH (Java Microbenchmark Harness) menangani semua jebakan di atas secara otomatis, sehingga developer dapat fokus pada kode yang ingin diukur daripada metodologi pengukuran.
Referensi Lanjutan #
- JMH (Java Microbenchmark Harness) — halaman proyek JMH di OpenJDK untuk menulis mikro-benchmark yang akurat di atas JVM
Selanjutnya #
Setelah memahami jebakan microbenchmark, kita siap memakai framework yang menanganinya secara otomatis. Bab berikutnya membahas JMH (Java Microbenchmark Harness) — standar de facto untuk benchmark di JVM. Materi tersebut ada di chapter JMH.