Profiling menunjukkan di mana waktu dihabiskan; benchmarking menjawab pertanyaan yang lebih tajam: seberapa cepat potongan kode ini, dan apakah versi A lebih cepat daripada versi B? Di kebanyakan bahasa, mengukur cukup dengan mengitari kode dengan stopwatch. Di JVM, cara itu menyesatkan secara sistematis, karena program yang diukur adalah mesin yang sedang berubah: compiler JIT bekerja di latar, menghapus dan menyusun ulang kode selama pengukuran berlangsung. Chapter ini membedah tiga jebakan klasik benchmark JVM — warmup, dead code elimination, dan pengukuran sekali jalan — dengan simulasi berbasis penghitung operasi yang bisa kita audit baris demi baris.
Mesin yang Berubah Saat Diukur #
Java dimulai dalam mode interpreter: bytecode diterjemahkan satu per satu, lambat tetapi langsung jalan. HotSpot kemudian mengamati method mana yang sering dieksekusi dan menyerahkannya ke Just-In-Time (JIT) compiler bertingkat — C1 mengompilasi cepat dengan optimasi sedang, C2 lebih lambat menyusun namun menghasilkan kode mesin jauh lebih rapat. Konsekuensinya: 100 eksekusi pertama sebuah method diukur dalam mode interpreter, sedangkan eksekusi ke-sejuta berjalan sebagai kode mesin teroptimasi. Benchmark yang tidak memisahkan dua fase itu melaporkan campuran keduanya — angka yang tidak mewakili operasi produksi mana pun. Javadoc System.nanoTime() sendiri mengingatkan bahwa durasi yang terukur bisa dipengaruhi aktivitas sistem di luar kendali kita.
Jebakan kedua lebih halus: JIT berhak menghapus pekerjaan yang hasilnya tidak pernah dipakai (dead code elimination). Loop yang menjumlahkan satu juta angka lalu membuang hasilnya bisa dihapus hampir seluruhnya — benchmark “0 milidetik” untuk pekerjaan yang sebenarnya tidak pernah dikerjakan.
Tiga Jebakan dalam Satu Program #
Program berikut memodelkan ketiga jebakan memakai satuan kerja dan pembukuan hasil — bukan milidetik — sehingga setiap angka bisa diverifikasi ulang dengan pensil:
/**
* Simulasi tiga jebakan benchmark JVM — warmup, dead code elimination,
* dan pengukuran sekali jalan — memakai penghitung satuan kerja dan
* pembukuan hasil, bukan pengukuran waktu sungguhan.
*/
public class PerangkapBenchmark {
static long lubangHitam = 0; // pengganti Blackhole JMH: penampung hasil
static int hasilTerpakai = 0;
static int kerja(int i) {
return (i * 31 + 7) % 1000;
}
public static void main(String[] args) {
// ---- Jebakan 1: warmup (model biaya bertingkat JIT) ----
System.out.println("== Jebakan 1: warmup (satuan kerja per eksekusi) ==");
long[] eksekusiKe = {1, 100, 10_000, 1_000_000};
String[] mode = {"interpreter ", "interpreter ", "compiled (C1) ", "compiled (C2) "};
int[] biaya = {3, 3, 1, 1};
System.out.printf("%10s %15s %6s%n", "eksekusi ke", "mode", "biaya");
for (int i = 0; i < eksekusiKe.length; i++) {
System.out.printf("%10d %15s %6d%n", eksekusiKe[i], mode[i], biaya[i]);
}
System.out.println("ukur 100 eksekusi pertama -> 3 satuan/eksekusi");
System.out.println("ukur setelah steady state -> 1 satuan/eksekusi");
System.out.println("selisih 3x tanpa satu baris kode berubah");
System.out.println();
// ---- Jebakan 2: dead code elimination ----
System.out.println("== Jebakan 2: dead code elimination ==");
int n = 2000;
// Harness naif: hasil dihitung lalu dibuang
for (int i = 0; i < n; i++) {
int hasilDibuang = kerja(i); // tidak pernah dibaca lagi
if (hasilDibuang == Integer.MIN_VALUE) {
System.out.println("tidak akan pernah tercetak");
}
}
System.out.println("harness naif : " + n + " eksekusi, hasil terpakai = "
+ hasilTerpakai + " -> pekerjaan rentan dihapus JIT");
// Harness benar: hasil dialirkan ke lubang hitam
for (int i = 0; i < n; i++) {
lubangHitam += kerja(i);
hasilTerpakai++;
}
System.out.println("harness benar : " + n + " eksekusi, hasil terpakai = "
+ hasilTerpakai + " (checksum " + lubangHitam + ")");
System.out.println();
// ---- Jebakan 3: satu pengukuran vs berulang ----
System.out.println("== Jebakan 3: satu angka vs beberapa angka ==");
int[] sampel = {7, 7, 8, 7, 9}; // "pengukuran" tetap, mis. satuan kerja
int satuAngka = sampel[sampel.length - 1];
int min = Integer.MAX_VALUE;
int maks = Integer.MIN_VALUE;
long total = 0;
for (int s : sampel) {
min = Math.min(min, s);
maks = Math.max(maks, s);
total += s;
}
double rerata = (double) total / sampel.length;
double deviasi = 100.0 * (satuAngka - rerata) / rerata;
System.out.println("satu pengukuran : " + satuAngka);
System.out.printf("5 pengukuran : min %d, rerata %.1f, maks %d%n", min, rerata, maks);
System.out.printf("kesimpulan satu angka menyimpang %.1f%% dari rerata%n", deviasi);
}
}Output yang Dihasilkan:
== Jebakan 1: warmup (satuan kerja per eksekusi) ==
eksekusi ke mode biaya
1 interpreter 3
100 interpreter 3
10000 compiled (C1) 1
1000000 compiled (C2) 1
ukur 100 eksekusi pertama -> 3 satuan/eksekusi
ukur setelah steady state -> 1 satuan/eksekusi
selisih 3x tanpa satu baris kode berubah
== Jebakan 2: dead code elimination ==
harness naif : 2000 eksekusi, hasil terpakai = 0 -> pekerjaan rentan dihapus JIT
harness benar : 2000 eksekusi, hasil terpakai = 2000 (checksum 999000)
== Jebakan 3: satu angka vs beberapa angka ==
satu pengukuran : 9
5 pengukuran : min 7, rerata 7.6, maks 9
kesimpulan satu angka menyimpang 18.4% dari rerataMari kita telaah baris-baris kuncinya:
- Baris 16-28 (tabel warmup): biaya per eksekusi dimodelkan menurun — 3 satuan saat interpreter, 1 satuan setelah JIT bekerja. Model ini meniru kurva nyata: tanpa satu baris kode berubah, “kecepatan” yang terukur naik tiga kali lipat hanya karena fase eksekusi berganti.
- Baris 34-42 (harness naif): hasil
kerja(i)disimpan ke variabelhasilDibuangyang tidak pernah dibaca. Barisifsesudahnya murni formalitas agar kode terlihat wajar bagi compilerjavac— pembukuan tetap mencatathasilTerpakai = 0. Inilah kondisi yang memicu JIT menghapus loop secara menyeluruh pada benchmark sungguhan. - Baris 44-50 (lubang hitam): setiap hasil ditampung ke variabel
lubangHitam— peran yang di JMH dipegang classBlackhole. Pembukuan kini mencatat 2000 dari 2000 hasil terpakai, dan checksum 999000 menjadi bukti pekerjaan benar-benar dikerjakan. - Baris 53-69 (satu angka vs beberapa angka): satu “pengukuran” bernilai 9 sedangkan rerata lima pengukuran 7,6 — kesimpulan dari satu angka menyimpang 18,4%. Angka pengukuran nyata berfluktuasi karena penjadwalan OS dan garbage collector, maka keputusan harus diambil dari distribusi, bukan satu titik.
Dari Stopwatch ke Harness #
Ketiga jebakan di atas menuntut perlakuan yang sudah terpolakan:
| Jebakan | Perlakuan harness yang benar |
|---|---|
| Warmup / JIT | eksekusi pemanasan dibuang, ukur hanya fase steady state |
| Dead code elimination | setiap hasil dialirkan ke blackhole dan dikonsumsi |
| Derau satu angka | ulangi pengukuran, laporkan rerata dan sebaran |
| Profil JIT berbeda tiap proses | jalankan tiap pengukuran di fork proses terpisah |
Menyusun harness yang benar secara manual itu mungkin, tetapi tiap detailnya sudah pernah salah dikerjakan orang. Kerangka kerja JMH dari OpenJDK mengemas keempat perlakuan di atas menjadi anotasi yang ringkas — pokok bahasan chapter berikutnya.
Referensi #
- Oracle: System.nanoTime() (Java SE 25)
- Oracle: Java Virtual Machine Technology Overview
- OpenJDK: JMH — Java Microbenchmark Harness
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PerangkapBenchmark.java
Unduh berkas/**
* Simulasi tiga jebakan benchmark JVM — warmup, dead code elimination,
* dan pengukuran sekali jalan — memakai penghitung satuan kerja dan
* pembukuan hasil, bukan pengukuran waktu sungguhan.
*/
public class PerangkapBenchmark {
static long lubangHitam = 0; // pengganti Blackhole JMH: penampung hasil
static int hasilTerpakai = 0;
static int kerja(int i) {
return (i * 31 + 7) % 1000;
}
public static void main(String[] args) {
// ---- Jebakan 1: warmup (model biaya bertingkat JIT) ----
System.out.println("== Jebakan 1: warmup (satuan kerja per eksekusi) ==");
long[] eksekusiKe = {1, 100, 10_000, 1_000_000};
String[] mode = {"interpreter ", "interpreter ", "compiled (C1) ", "compiled (C2) "};
int[] biaya = {3, 3, 1, 1};
System.out.printf("%10s %15s %6s%n", "eksekusi ke", "mode", "biaya");
for (int i = 0; i < eksekusiKe.length; i++) {
System.out.printf("%10d %15s %6d%n", eksekusiKe[i], mode[i], biaya[i]);
}
System.out.println("ukur 100 eksekusi pertama -> 3 satuan/eksekusi");
System.out.println("ukur setelah steady state -> 1 satuan/eksekusi");
System.out.println("selisih 3x tanpa satu baris kode berubah");
System.out.println();
// ---- Jebakan 2: dead code elimination ----
System.out.println("== Jebakan 2: dead code elimination ==");
int n = 2000;
// Harness naif: hasil dihitung lalu dibuang
for (int i = 0; i < n; i++) {
int hasilDibuang = kerja(i); // tidak pernah dibaca lagi
if (hasilDibuang == Integer.MIN_VALUE) {
System.out.println("tidak akan pernah tercetak");
}
}
System.out.println("harness naif : " + n + " eksekusi, hasil terpakai = "
+ hasilTerpakai + " -> pekerjaan rentan dihapus JIT");
// Harness benar: hasil dialirkan ke lubang hitam
for (int i = 0; i < n; i++) {
lubangHitam += kerja(i);
hasilTerpakai++;
}
System.out.println("harness benar : " + n + " eksekusi, hasil terpakai = "
+ hasilTerpakai + " (checksum " + lubangHitam + ")");
System.out.println();
// ---- Jebakan 3: satu pengukuran vs berulang ----
System.out.println("== Jebakan 3: satu angka vs beberapa angka ==");
int[] sampel = {7, 7, 8, 7, 9}; // "pengukuran" tetap, mis. satuan kerja
int satuAngka = sampel[sampel.length - 1];
int min = Integer.MAX_VALUE;
int maks = Integer.MIN_VALUE;
long total = 0;
for (int s : sampel) {
min = Math.min(min, s);
maks = Math.max(maks, s);
total += s;
}
double rerata = (double) total / sampel.length;
double deviasi = 100.0 * (satuAngka - rerata) / rerata;
System.out.println("satu pengukuran : " + satuAngka);
System.out.printf("5 pengukuran : min %d, rerata %.1f, maks %d%n", min, rerata, maks);
System.out.printf("kesimpulan satu angka menyimpang %.1f%% dari rerata%n", deviasi);
}
}