Chapter sebelumnya menutup dengan kesimpulan bahwa harness benchmark yang benar perlu pemanasan, konsumsi hasil, pengulangan, dan isolasi proses. Kebutuhan itu sudah menjadi tugas penuh sebuah kerangka kerja: JMH (Java Microbenchmark Harness), proyek OpenJDK yang dipakai tim JVM sendiri untuk mengukur kinerja perubahan internal HotSpot. Pada chapter ini kita pelajari anotasi inti JMH, memahami arti warmup, fork, dan blackhole, lalu membuktikan pola kerjanya dengan harness mini yang kita rakit sendiri dari Java murni — karena konsepnya jauh lebih mudah dicerna ketika setiap tahap terlihat sebagai kode biasa.
Anatomi Sebuah Benchmark JMH #
JMH mengambil alih seluruh siklus pengukuran lewat anotasi. Berikut bentuk benchmark yang membandingkan penjumlahan array dengan perulangan melawan IntStream:
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.concurrent.TimeUnit;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class PenjumlahanBench {
int[] data;
@Setup
public void siapkan() {
data = new int[10_000];
for (int i = 0; i < data.length; i++) {
data[i] = i % 1000;
}
}
@Benchmark
public void penjumlahanLoop(Blackhole bh) {
long total = 0;
for (int n : data) {
total += n;
}
bh.consume(total);
}
@Benchmark
public long penjumlahanStream() {
return java.util.Arrays.stream(data).sum();
}
}Setiap anotasi memegang satu keputusan desain: @State mengatur visibilitas status antar thread, @Setup menyiapkan data sekali sebelum pengukuran, @Warmup membuang iterasi pemanasan dari laporan, @Measurement mengatur iterasi yang dihitung, dan @Fork(2) menjalankan seluruh benchmark dua kali di proses JVM terpisah — tiap fork mendapat profil JIT yang segar sehingga urutan eksekusi method tidak mencemari hasil. Method bertanda @Benchmark adalah objek ukur; method kedua mengembalikan nilai, dan JMH otomatis memperlakukan nilai kembali tersebut sebagai hasil yang dikonsumsi, sedangkan method pertama memakai Blackhole secara eksplisit.
Pola Kerja JMH dalam Harness Mini #
JMH dibundel lewat dependency Maven atau Gradle — pada lingkungan course ini kode JMH di atas tampil sebagai contoh tanpa klaim hasil eksekusi. Pola kerjanya tetap bisa kita buktikan dengan harness mini berbasis penghitung operasi:
import java.util.Random;
/**
* Harness mini buatan tangan yang meniru kerangka JMH: fase pemanasan,
* fase pengukuran berstatistik, dan lubang hitam (blackhole) pengonsumsi
* hasil. "Derau" dimodelkan Random ber-seed sehingga output deterministik.
*/
public class HarnessMini {
static long lubangHitam = 0; // pengganti Blackhole JMH
static int hasilTerpakai = 0; // pembukuan hasil agar terlihat hidup
static int kerja(int i) {
return (i * 37 + 11) % 1000;
}
// Satu iterasi benchmark: seluruh hasil dialirkan ke lubang hitam
static long jalankanBatch(int jumlahOp) {
long checksum = 0;
for (int i = 0; i < jumlahOp; i++) {
int r = kerja(i);
lubangHitam += r;
checksum += r;
hasilTerpakai++;
}
return checksum;
}
// Derau penjadwalan tiruan: jumlah op nyata bergerak 97%-103% dari dasar
static int opUntukIterasi(int dasar, Random derau) {
int persen = 100 + derau.nextInt(7) - 3;
return dasar * persen / 100;
}
public static void main(String[] args) {
int iterasiPemanasan = 3; // padanan @Warmup(iterations = 3)
int iterasiUkur = 5; // padanan @Measurement(iterations = 5)
int opDasar = 10_000;
Random derau = new Random(42);
System.out.println("Konfigurasi : " + iterasiPemanasan + " iterasi pemanasan + "
+ iterasiUkur + " iterasi pengukuran, " + opDasar + " op dasar");
// Fase 1: pemanasan. Pekerjaan tetap dikonsumsi, angka dibuang.
long opPemanasan = 0;
for (int w = 0; w < iterasiPemanasan; w++) {
int op = opUntukIterasi(opDasar, derau);
opPemanasan += op;
jalankanBatch(op);
}
System.out.println("Pemanasan : " + iterasiPemanasan + " iterasi, "
+ opPemanasan + " op dijalankan (tidak masuk laporan)");
System.out.println("Pengukuran :");
// Fase 2: pengukuran dengan statistik
int[] opPerIterasi = new int[iterasiUkur];
for (int it = 0; it < iterasiUkur; it++) {
opPerIterasi[it] = opUntukIterasi(opDasar, derau);
long checksum = jalankanBatch(opPerIterasi[it]);
System.out.println(" iterasi " + (it + 1) + " : " + opPerIterasi[it]
+ " op, checksum " + checksum);
}
int min = Integer.MAX_VALUE;
int maks = Integer.MIN_VALUE;
long total = 0;
for (int op : opPerIterasi) {
min = Math.min(min, op);
maks = Math.max(maks, op);
total += op;
}
double rerata = (double) total / iterasiUkur;
double sebaran = 100.0 * (maks - min) / rerata;
System.out.printf("Statistik : rerata %.1f op, min %d, maks %d, sebaran %.2f%%%n",
rerata, min, maks, sebaran);
System.out.println("Blackhole : " + hasilTerpakai + " hasil dikonsumsi dari "
+ hasilTerpakai + " pekerjaan (100%)");
}
}Output yang Dihasilkan:
Konfigurasi : 3 iterasi pemanasan + 5 iterasi pengukuran, 10000 op dasar
Pemanasan : 3 iterasi, 30300 op dijalankan (tidak masuk laporan)
Pengukuran :
iterasi 1 : 10000 op, checksum 4995000
iterasi 2 : 10200 op, checksum 5089500
iterasi 3 : 10100 op, checksum 5041250
iterasi 4 : 9800 op, checksum 4892500
iterasi 5 : 10000 op, checksum 4995000
Statistik : rerata 10020.0 op, min 9800, maks 10200, sebaran 3.99%
Blackhole : 80400 hasil dikonsumsi dari 80400 pekerjaan (100%)Mari kita telaah padanannya satu per satu:
- Baris 44-52 (fase pemanasan): tiga iterasi dijalankan penuh — hasil tetap masuk
lubangHitamsupaya pekerjaan tidak hilang — tetapi angkanya sengaja tidak dilaporkan. Inilah@Warmup(iterations = 3)versi mini: membuang fase interpreter dan kompilasi JIT dari statistik. - Baris 55-62 (fase pengukuran): lima iterasi dicatat satu per satu lengkap dengan checksum — padanan
@Measurement(iterations = 5). Checksum yang berbeda-beda membuktikan jumlah pekerjaan nyata tiap iterasi tidak identik. - Baris 29-33 (
opUntukIterasi): derau penjadwalan dimodelkanRandomber-seed sehingga jumlah op bergerak di rentang 97%-103%. Pada benchmark sungguhan derau datang dari OS dan GC; di sini derau disintesis agar statistik di baris 64-75 punya arti yang bisa diaudit. - Baris 10-11 dan 17-27 (
lubangHitam): seluruh hasilkerja(i)ditampung sebelum dipakai — peranBlackhole.consume()yang mencegah dead code elimination. Baris terakhir output, 80.400 hasil terkonsumsi, adalah jaminan pembukuan bahwa tidak ada pekerjaan yang menguap tanpa jejak.
Mode Pengukuran dan Profiler #
| Mode JMH | Pertanyaan yang dijawab | Satuan laporan |
|---|---|---|
Mode.AverageTime | berapa lama satu operasi rata-rata | nanodetik/op |
Mode.Throughput | berapa operasi per satuan waktu | op/detik |
Mode.SampleTime | sebaran latensi hingga persentil | histogram |
Mode.SingleShotTime | biaya sekali jalan (cold) | nanodetik |
Mode dipilih sesuai pertanyaan bisnis — AverageTime untuk biaya per panggilan, Throughput untuk kapasitas agregat; perbedaan keduanya dibedah tuntas pada Chapter Latency vs Throughput. JMH juga membawa profiler bawaan: -prof gc melaporkan alokasi per operasi, dan -prof jfr merekam eksekusi ke berkas Java Flight Recorder yang bisa dianalisis lanjut — jembatan langsung ke chapter CPU profiling berikutnya.
Harness mini kita mengukur jumlah operasi; JMH mengukur waktu di atas proses ter-fork dengan statistik yang jauh lebih kaya. Namun angka benchmark, sebaik apa pun harnessnya, tetap hanya menjawab “berapa cepat” — untuk menemukan mengapa lambat kita perlu mengintip ke dalam eksekusi. Chapter berikutnya masuk ke CPU profiling: bagaimana sampling profiler menemukan method yang memakan waktu.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
HarnessMini.java
Unduh berkasimport java.util.Random;
/**
* Harness mini buatan tangan yang meniru kerangka JMH: fase pemanasan,
* fase pengukuran berstatistik, dan lubang hitam (blackhole) pengonsumsi
* hasil. "Derau" dimodelkan Random ber-seed sehingga output deterministik.
*/
public class HarnessMini {
static long lubangHitam = 0; // pengganti Blackhole JMH
static int hasilTerpakai = 0; // pembukuan hasil agar terlihat hidup
static int kerja(int i) {
return (i * 37 + 11) % 1000;
}
// Satu iterasi benchmark: seluruh hasil dialirkan ke lubang hitam
static long jalankanBatch(int jumlahOp) {
long checksum = 0;
for (int i = 0; i < jumlahOp; i++) {
int r = kerja(i);
lubangHitam += r;
checksum += r;
hasilTerpakai++;
}
return checksum;
}
// Derau penjadwalan tiruan: jumlah op nyata bergerak 97%-103% dari dasar
static int opUntukIterasi(int dasar, Random derau) {
int persen = 100 + derau.nextInt(7) - 3;
return dasar * persen / 100;
}
public static void main(String[] args) {
int iterasiPemanasan = 3; // padanan @Warmup(iterations = 3)
int iterasiUkur = 5; // padanan @Measurement(iterations = 5)
int opDasar = 10_000;
Random derau = new Random(42);
System.out.println("Konfigurasi : " + iterasiPemanasan + " iterasi pemanasan + "
+ iterasiUkur + " iterasi pengukuran, " + opDasar + " op dasar");
// Fase 1: pemanasan. Pekerjaan tetap dikonsumsi, angka dibuang.
long opPemanasan = 0;
for (int w = 0; w < iterasiPemanasan; w++) {
int op = opUntukIterasi(opDasar, derau);
opPemanasan += op;
jalankanBatch(op);
}
System.out.println("Pemanasan : " + iterasiPemanasan + " iterasi, "
+ opPemanasan + " op dijalankan (tidak masuk laporan)");
System.out.println("Pengukuran :");
// Fase 2: pengukuran dengan statistik
int[] opPerIterasi = new int[iterasiUkur];
for (int it = 0; it < iterasiUkur; it++) {
opPerIterasi[it] = opUntukIterasi(opDasar, derau);
long checksum = jalankanBatch(opPerIterasi[it]);
System.out.println(" iterasi " + (it + 1) + " : " + opPerIterasi[it]
+ " op, checksum " + checksum);
}
int min = Integer.MAX_VALUE;
int maks = Integer.MIN_VALUE;
long total = 0;
for (int op : opPerIterasi) {
min = Math.min(min, op);
maks = Math.max(maks, op);
total += op;
}
double rerata = (double) total / iterasiUkur;
double sebaran = 100.0 * (maks - min) / rerata;
System.out.printf("Statistik : rerata %.1f op, min %d, maks %d, sebaran %.2f%%%n",
rerata, min, maks, sebaran);
System.out.println("Blackhole : " + hasilTerpakai + " hasil dikonsumsi dari "
+ hasilTerpakai + " pekerjaan (100%)");
}
}