Profil dari chapter sebelumnya menunjukkan bahwa sebagian besar waktu permintaan habis di satu fungsi, dan kita punya dua ide perbaikan. Mana yang lebih cepat — dan seberapa yakin kita dengan jawaban itu? Benchmark menjawab pertanyaan tersebut dengan mengukur total waktu eksekusi secara terkontrol. Pada chapter ini kita membangun harness micro-benchmark berbasis hrtime(), membandingkan dua strategi penyambungan string secara adil, lalu membahas jebakan yang membuat banyak benchmark di internet layak diragukan.
Tiga Prinsip Desain Benchmark #
Benchmark yang bisa dipercaya selalu berdiri di atas tiga prinsip. Pertama, isolasi: ubah satu variabel saja — kedua kandidat harus mengerjakan pekerjaan yang persis sama dengan input yang persis sama. Kedua, warmup: jalankan kode sekali tanpa diukur lebih dulu, karena eksekusi pertama membayar biaya yang tidak akan muncul lagi seperti pemuatan class dan kompilasi berkas. Ketiga, repetisi: ulangi pengukuran beberapa kali dan laporkan angka stabil (minimal atau median), karena satu kali pengukuran bisa saja kebetulan terganggu proses lain di mesin yang sama.
Prinsip-prinsip ini juga menjelaskan mengapa satu angka benchmark dari blog orang lain tidak bisa langsung dipercaya begitu saja: kita tidak tahu versi PHP, konfigurasi, dan mesin apa yang dipakai penulisnya.
Harness Micro-Benchmark dengan hrtime #
Mari kita bangun harnessnya, lalu memakainya untuk membandingkan dua cara membuat string berulang: menyambung satu per satu dalam perulangan versus satu panggilan str_repeat():
<?php
function benchmarkSederhana(int $jumlahUlang, callable $kode): int
{
$kode(); // warmup: satu kali dijalankan tanpa diukur
$terbaik = PHP_INT_MAX;
for ($ulang = 0; $ulang < $jumlahUlang; $ulang++) {
$mulai = hrtime(true);
$kode();
$selisih = hrtime(true) - $mulai;
$terbaik = min($terbaik, $selisih);
}
return $terbaik; // waktu terbaik dalam nanodetik
}
$buatDenganLoop = function (): string {
$hasil = "";
for ($i = 0; $i < 2000; $i++) {
$hasil .= "ab";
}
return $hasil;
};
$buatDenganStrRepeat = function (): string {
return str_repeat("ab", 2000);
};
$waktuLoop = benchmarkSederhana(5, $buatDenganLoop);
$waktuStrRepeat = benchmarkSederhana(5, $buatDenganStrRepeat);
// Pemeriksaan keadilan: kedua strategi wajib menghasilkan string identik
$hasilLoop = $buatDenganLoop();
$hasilStrRepeat = $buatDenganStrRepeat();
echo "Kedua strategi menghasilkan string identik: ";
var_dump($hasilLoop === $hasilStrRepeat);
echo "Panjang string hasil: " . strlen($hasilLoop) . " karakter\n";
echo "Jumlah pengukuran per strategi: 5\n";
echo "str_repeat lebih cepat daripada penyambungan berulang: ";
var_dump($waktuStrRepeat < $waktuLoop);Output yang Dihasilkan:
Kedua strategi menghasilkan string identik: bool(true)
Panjang string hasil: 4000 karakter
Jumlah pengukuran per strategi: 5
str_repeat lebih cepat daripada penyambungan berulang: bool(true)Mari kita bedah harness dan pemakaiannya:
- Baris 2-14 (
benchmarkSederhana): menerima jumlah pengulangan dan sebuahcallable. Baris 4 menjalankan warmup tanpa diukur; baris 7-12 mengukur tiap eksekusi dengan sepasanghrtime(true)dan menyimpan yang tercepat lewatmin(); baris 13 mengembalikan nanodetik terbaik. - Baris 16-26: dua kandidat berbentuk closure — keduanya membuat string
"ab"sebanyak 2.000 kali dengan jumlah iterasi yang sama persis. - Baris 28-29: kedua kandidat diukur dengan jumlah pengulangan dan perlakuan yang identik — inilah wujud isolasi.
- Baris 31-36: hasil kedua kandidat dibandingkan dengan
===. Strategi yang menghasilkan output berbeda tidak sah disebut “lebih cepat”; ia hanya salah. - Baris 38-41: laporan hanya mencetak fakta struktural — hasil identik, panjang string, jumlah pengukuran, dan siapa pemenangnya. Angka nanodetik sengaja tidak dicetak karena berubah antar mesin; pada pengujian kami
str_repeat()menang lebih dari seratus kali lipat karena dialokasikan sekali, sementara perulangan melakukan 2.000 kali penyambungan yang menyalin ulang string yang terus membesar.
Membaca Hasil dengan Jujur #
Satu eksekusi tunggal bukanlah hasil; ia adalah sampel pertama dari sekumpulan sampel yang bervariasi. Kode berikut mengambil tujuh sampel dan membandingkan minimum, median, serta maksimumnya:
<?php
function medianSampel(array $sampelNanodetik): float
{
sort($sampelNanodetik);
$jumlah = count($sampelNanodetik);
$tengah = intdiv($jumlah, 2);
if ($jumlah % 2 === 1) {
return (float) $sampelNanodetik[$tengah];
}
return ($sampelNanodetik[$tengah - 1] + $sampelNanodetik[$tengah]) / 2.0;
}
$sampel = [];
$jumlahKarakter = 0;
for ($ulang = 0; $ulang < 7; $ulang++) {
$mulai = hrtime(true);
$hasil = str_repeat("php", 100000);
$sampel[] = hrtime(true) - $mulai;
$jumlahKarakter += strlen($hasil);
}
$minimum = min($sampel);
$median = medianSampel($sampel);
$maksimum = max($sampel);
echo "Jumlah sampel: " . count($sampel) . "\n";
echo "Total karakter terhitung: " . $jumlahKarakter . "\n";
echo "minimum <= median: ";
var_dump($minimum <= $median);
echo "median <= maksimum: ";
var_dump($median <= $maksimum);
echo "minimum < maksimum (variasi antar eksekusi itu nyata): ";
var_dump($minimum < $maksimum);Output yang Dihasilkan:
Jumlah sampel: 7
Total karakter terhitung: 2100000
minimum <= median: bool(true)
median <= maksimum: bool(true)
minimum < maksimum (variasi antar eksekusi itu nyata): bool(true)Fungsi medianSampel() pada baris 2-11 mengurutkan sampel lalu mengambil nilai tengah (atau rata-rata dua nilai tengah bila jumlahnya genap). Perulangan pada baris 15-20 mengambil tujuh sampel — perhatikan bahwa hasil str_repeat() tetap dikonsumsi oleh strlen() pada baris 19, menjaga agar pekerjaan yang diukur tidak pernah berkurang. Hasilnya menegaskan dua hal: minimum <= median <= maksimum selalu berlaku, dan minimum < maksimum membuktikan variasi antar eksekusi itu nyata — itulah mengapa melaporkan satu angka dari satu eksekusi bersifat menyesatkan. Minimum sering dipakai sebagai perkiraan “biaya sebenarnya” karena paling sedikit terganggu, sementara median lebih tahan terhadap pencilan dibanding rata-rata.
Jebakan Umum Micro-Benchmark #
Beberapa jebakan muncul berulang kali dan patut dihafal. Hasil yang tidak dikonsumsi: berbeda dari compiler C yang berhak menghapus kode mati, engine PHP mengeksekusi opcode apa adanya — perulangan yang hasilnya tidak dipakai tetap dikerjakan penuh. Meski begitu, tetap konsumsi hasilnya seperti contoh di atas agar kedua kandidat diperlakukan setara dan kode Anda tetap relevan bila suatu saat engine berubah. Eksekusi pertama vs keadaan stabil: pemuatan class lewat autoloader dan pembukaan koneksi hanya terjadi sekali; tanpa warmup, kandidat yang kebetulan diukur lebih dulu tampak lebih lambat. JIT dan konfigurasi: JIT sejak PHP 8.0 dapat mempercepat kode numerik yang padat CPU, tetapi hampir tidak berpengaruh untuk aplikasi web yang menunggu I/O — hasil bisa berbeda antar konfigurasi, jadi sebutkan versi dan pengaturan saat melaporkan. Lingkungan yang bising: proses latar belakang, pembatasan daya laptop, dan mesin yang berbeda mengubah angka absolut; bandingkan hanya hasil dari mesin dan sesi yang sama.
Membandingkan Implementasi secara Adil #
Keadilan berarti kedua kandidat mengerjakan definisi pekerjaan yang sama, diverifikasi dengan perbandingan hasil seperti pada baris 31-36 harness kita. Satu aspek yang sering dilupakan adalah ukuran data yang realistis: pemenang bisa bergantung pada skala, karena strategi dan kompleksitas algoritmanya berbeda. Contoh klasiknya mencari nilai di kumpulan data:
| Ukuran data | Strategi yang biasanya menang | Alasan |
|---|---|---|
| 10 item | in_array() biasa | Biaya menyiapkan struktur alternatif lebih besar dari untungnya |
| 10.000 item | isset() pada array yang dibalik | Pencarian hash O(1) mengalahkan pemindaian linear O(n) |
Tabel di atas adalah ilustrasi pola, bukan angka sakral — angka pastinya berbeda-beda tergantung mesin dan beban, dan itulah mengapa Anda perlu mengukur pada ukuran data yang benar-benar mewakili aplikasi. Menyimpulkan dari data 10 item lalu diterapkan pada tabel berisi jutaan baris adalah kesalahan desain, bukan sekadar kesalahan angka.
Macro Benchmark #
Micro-benchmark mengukur potongan kode dalam isolasi; ia tidak mengatakan apa pun tentang aplikasi utuh. Macro benchmark mengukur permintaan HTTP dari luar — waktu tanggapan p95 saat dihujani beban yang menyerupai traffic asli, menggunakan alat seperti ab, wrk, atau k6 di lingkungan staging dengan data sebesar produksi. Pola kerjanya sama dengan loop optimasi pada chapter pertama: ukur baseline, terapkan perubahan, ukur lagi di kondisi yang sama. Kombinasi idealnya bertingkat: macro benchmark menemukan bahwa aplikasi lambat, profiling menemukan fungsinya, micro-benchmark memilihkan implementasi penggantinya.
Ringkasan #
Benchmark yang jujur mengisolasi satu perubahan, memanaskan kode sebelum diukur, mengulang pengukuran, memverifikasi hasil kedua kandidat identik, dan melaporkan minimum atau median — bukan satu angka dari satu eksekusi. Jebakan seperti hasil yang tidak dikonsumsi, eksekusi pertama yang belum stabil, dan ukuran data yang tidak realistis bisa membalikkan kesimpulan. Setelah implementasi tercepat dipilih dan profil menunjukkan biaya kompilasi berkas yang berulang, saatnya membahas lapisan optimasi pertama yang wajib di produksi: OPcache pada chapter berikutnya.
Referensi #
- PHP: hrtime — dasar pengukuran waktu beresolusi nanodetik yang dipakai seluruh harness chapter ini.
- PHP: The Right Way — budaya umum mengukur sebelum mengoptimalkan di ekosistem PHP.