Topik ini memperkenalkan dua alat yang memeriksa kode di luar kebenaran hasil, yaitu benchmark untuk mengukur kinerja dan fuzzing untuk memburu panic yang dipicu masukan tak terduga. Pembahasan mencakup kerangka Criterion sebagai kebiasaan umum benchmark pada toolchain stabil, peran cargo fuzz sebagai pembungkus libFuzzer yang terbimbing cakupan, alasan keduanya hanya ditampilkan sebagai cuplikan yang tidak dijalankan, serta dua program kecil tanpa crate eksternal yang memperagakan inti kedua konsep secara deterministik. Setelah membacanya, pengukuran kinerja dapat disusun dengan sadar akan optimalisasi kompiler, dan siklus fuzzing dapat dijelaskan tanpa menunggu instalasi alat tambahan.
Benchmark dengan kerangka Criterion#
Benchmark mengukur lama eksekusi sebuah fungsi, bukan kebenaran hasilnya. Rust menyediakan makro #[bench] bawaan, tetapi makro itu hanya tersedia pada toolchain nightly, sehingga kerangka Criterion menjadi pilihan yang umum pada toolchain stabil. Criterion menjalankan fungsi berulang kali, mengumpulkan sampel waktu, lalu melaporkan statistiknya serta perbandingannya terhadap hasil pengukuran sebelumnya yang tersimpan pada direktori target/criterion.
Pemasangan Criterion dilakukan sebagai dependensi pengembangan. Satu berkas benchmark didaftarkan dengan harness = false agar titik masuk program menjadi main buatan Criterion, bukan pelari pengujian bawaan.
# tidak-dijalankan
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "penghitung_kata"
harness = false// tidak-dijalankan
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use penghitung_kata::hitung_kata;
fn bench_hitung(c: &mut Criterion) {
let teks = "penghitung kata sederhana untuk latihan benchmark pada jalur rust";
c.bench_function("hitung_kata", |b| b.iter(|| hitung_kata(black_box(teks))));
}
criterion_group!(benches, bench_hitung);
criterion_main!(benches);Makro criterion_group! dan criterion_main! membentuk main yang menjalankan seluruh pengukuran terdaftar, dan perintah cargo bench menjalankannya. Metode bench_function menerima nama pengukuran beserta closure. Di dalam closure tersebut, b.iter menerima closure lain dan menjalankannya secara berulang. Fungsi black_box membungkus argumen agar kompiler memperlakukan isinya sebagai tidak diketahui, sehingga pemanggilan tidak dapat dihitung pada waktu kompilasi atau dihapus sebagai pekerjaan yang tidak berdampak.
Keluaran Criterion berupa tabel yang memuat waktu, selisih terhadap pengukuran sebelumnya, dan nilai p statistiknya. Kedua cuplikan di atas ditandai tidak-dijalankan karena Criterion adalah crate eksternal yang hanya dapat diunduh oleh Cargo. Keluaran waktunya juga berubah pada setiap jalankan serta berbeda pada setiap mesin, sehingga tidak digandakan ke dalam blok keluaran.
Pengukuran struktural tanpa crate eksternal#
Sebelum waktu diukur, struktur pekerjaan sudah menentukan hasilnya. Program berikut membandingkan dua implementasi fibonacci dengan hasil sama, yaitu versi rekursif yang menghitung ulang setiap cabang dan versi iteratif yang menggeser dua nilai terakhir. Alat ukurnya bukan waktu melainkan jumlah satuan kerja, yakni banyaknya pemanggilan fungsi pada versi rekursif dan banyaknya putaran perulangan pada versi iteratif, sehingga keluarannya deterministik.
// Perbandingan struktural dua implementasi fibonacci tanpa crate eksternal.
use std::hint::black_box;
use std::sync::atomic::{AtomicUsize, Ordering};
static PANGGIL_NAIF: AtomicUsize = AtomicUsize::new(0);
/// Fibonacci rekursif yang menghitung ulang seluruh cabang.
fn fib_naif(n: u64) -> u64 {
PANGGIL_NAIF.fetch_add(1, Ordering::Relaxed);
match n {
0 | 1 => n,
_ => fib_naif(n - 1) + fib_naif(n - 2),
}
}
/// Fibonacci iteratif yang menyimpan dua nilai terakhir.
fn fib_iteratif(n: u64) -> u64 {
let (mut a, mut b) = (0u64, 1u64);
for _ in 0..n {
let berikutnya = a + b;
a = b;
b = berikutnya;
}
a
}
fn main() {
let n = 25;
let hasil_naif = fib_naif(black_box(n));
let panggilan = PANGGIL_NAIF.load(Ordering::Relaxed);
let hasil_iteratif = fib_iteratif(black_box(n));
println!("fib({n}) rekursif : {hasil_naif}, {panggilan} pemanggilan fungsi");
println!("fib({n}) iteratif : {hasil_iteratif}, {n} putaran perulangan");
}fib(25) rekursif : 75025, 242785 pemanggilan fungsi
fib(25) iteratif : 75025, 25 putaran perulanganKedua versi menghasilkan 75025, tetapi versi rekursif melakukan 242785 pemanggilan fungsi sedangkan versi iteratif cukup dengan 25 putaran. Pada mesin apa pun, waktu versi rekursif mengikuti jumlah pemanggilan itu, sehingga jurang hampir sepuluh ribu kali lipat akan muncul pada pengukuran waktu apa pun. Pemakaian black_box pada masukan menjaga kedua pemanggilan tetap dievaluasi saat program berjalan alih-alih dihitung oleh kompiler sebelum program dimulai, dan pemakaian inilah yang dilakukan Criterion dalam setiap iter.
Fuzzing terbimbing cakupan dengan cargo fuzz#
Fuzzing menjalankan sebuah fungsi sasaran dengan masukan acak yang terus dibarukan, lalu mengawasi apakah fungsi itu panic atau gagal. Fuzzer modern tidak mengacak secara buta. Mesin libFuzzer mengamati jalur kode yang dilalui setiap masukan, menyimpan masukan yang menjangkau jalur baru ke dalam korpus, dan memakai isi korpus sebagai bibit untuk mutasi berikutnya. Pencarian menjadi terarah karena mutasi dilakukan atas masukan yang terbukti menjelajah lebih dalam.
Alat cargo fuzz membungkus libFuzzer agar dapat dipakai dari Cargo. Ia menambahkan subcommand sendiri yang dipasang lewat cargo install, menyiapkan direktori fuzz/ berisi target sasaran, dan menjalankan mesinnya. Dua cuplikan berikut ditandai tidak-dijalankan karena pemasangan cargo-fuzz memerlukan unduhan crate eksternal dan kompilasi target sasaran memerlukan toolchain nightly.
# tidak-dijalankan
cargo install cargo-fuzz
cargo fuzz init
cargo fuzz run fuzz_target_1// tidak-dijalankan
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if let Ok(teks) = std::str::from_utf8(data) {
penghitung_kata::hitung_kata(teks);
}
});Makro fuzz_target! menerima closure yang dipanggil dengan masukan mentah &[u8] pada setiap iterasi. Konversi ke &str yang gagal cukup diabaikan karena masukan acak memang tidak selalu merupakan teks yang sah, sedangkan masukan yang sah diteruskan ke fungsi sasaran. Ketika panic terjadi, libFuzzer menyimpan masukan pemantiknya pada direktori fuzz/artifacts dan berhenti, sehingga masukan itu dapat ditambahkan sebagai kasus pengujian regresi dan kegagannya diperiksa sebelum fuzzing dilanjutkan.
Fuzzer mini tanpa crate eksternal#
Siklus inti fuzzing dapat ditiru dengan pustaka standar saja. Program berikut memuat fungsi sasaran diakhiri_seru yang memeriksa huruf terakhir sebuah teks, sebuah generator masukan berupa linear congruential generator dengan benih tetap, dan pengawas berupa catch_unwind yang menangkap panic tanpa menghentikan program. Benih yang tetap membuat seluruh rangkaian masukan dapat direproduksi, dan pencetakan panic bawaan dimatikan agar laporan hanya berasal dari siklus ini.
// Fuzzer mini: meniru siklus fuzzing dengan generator masukan deterministik.
use std::panic::{catch_unwind, AssertUnwindSafe};
/// Fungsi sasaran: memeriksa apakah teks diakhiri tanda seru.
///
/// Masukan kosong membuat perhitungan indeks terakhir meluap dan memicu panic.
fn diakhiri_seru(teks: &str) -> bool {
let huruf: Vec<char> = teks.chars().collect();
huruf[huruf.len() - 1] == '!'
}
/// Satu langkah generator LCG (linear congruential generator).
fn langkah(benih: u64) -> u64 {
benih
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407)
}
fn main() {
// matikan pencetakan panic bawaan agar laporan hanya berasal dari siklus ini
std::panic::set_hook(Box::new(|_| {}));
let mut benih: u64 = 2026;
let mut iterasi = 0;
loop {
iterasi += 1;
benih = langkah(benih);
let panjang = ((benih >> 33) % 5) as usize;
let mut masukan = String::new();
for _ in 0..panjang {
benih = langkah(benih);
masukan.push(if ((benih >> 33) & 1) == 1 { 'a' } else { '!' });
}
let hasil = catch_unwind(AssertUnwindSafe(|| diakhiri_seru(&masukan)));
if hasil.is_err() {
println!("panic pada iterasi {iterasi} dengan masukan {masukan:?}");
return;
}
}
}panic pada iterasi 11 dengan masukan ""Generator membangun teks sepanjang nol sampai empat karakter dari huruf a dan !, dan pada iterasi kesebelas panjangnya nol sehingga masukannya berupa teks kosong. Fungsi sasaran menghitung huruf[huruf.len() - 1], dan pengurangan 0 - 1 pada tipe usize meluap ketika potongan kosong, sehingga panic terjadi dan ditangkap pengawas. Diagnosisnya sama seperti temuan libFuzzer pada umumnya, yaitu masukan terpendek yang mungkin, dan perbaikannya mengganti pengindeksan dengan teks.chars().next_back() == Some('!') yang menghasilkan false untuk teks kosong tanpa panic.
Pemetaan ke fuzzer sesungguhnya berlaku satu banding satu. Generator LCG berperan sebagai mutasi acak, catch_unwind sebagai pengawas, dan masukan yang tercetak berperan sebagai artefak yang disimpan libFuzzer pada fuzz/artifacts. Bedanya, fuzzer mini ini tidak memiliki pembimbing cakupan dan tidak akan berhenti bila fungsi sasarannya benar, sedangkan libFuzzer dibatasi waktu serta diarahkan oleh korpus. Sifat deterministik menjadikan fuzzer mini cocok untuk memahami mekanisme fuzzing, sedangkan cargo fuzz untuk benar-benar memburu kegagalan pada pustaka nyata.
Kesalahan umum#
Mengukur tanpa black_box. Closure yang menerima nilai konstanta dapat dihitung kompiler pada waktu kompilasi atau dihapus seluruhnya, sehingga pengukuran melaporkan waktu nyaris nol untuk pekerjaan yang tidak pernah dikerjakan. Bungkus masukan dan hasil dengan black_box agar dievaluasi tetap terjadi saat program berjalan.
Menuntut keluaran waktu yang persis sama. Waktu benchmark berubah antar jalankan dan berbeda antar mesin, sehingga membandingkan karakter demi karakter selalu gagal. Gunakan statistik yang disediakan kerangka, dan perlakukan ambang waktu absolut pada pipeline dengan hati-hati karena mesin berbagi beban dapat melambat sewaktu-waktu.
Menyamakan temuan fuzzing dengan kebenaran nilai. Fuzzing mendeteksi panic dan gagal program, bukan memeriksa hasil yang benar. Fungsi yang mengembalikan nilai keliru tanpa panic dianggap sehat oleh fuzzer, sehingga asersi atas nilai tetap menjadi tugas unit test dan integration test.
Ringkasan#
- Criterion menjadi kerangka benchmark yang umum pada toolchain stabil, menjalankan pengukuran dengan statistik serta perbandingan terhadap hasil simpanan, dan dipasang sebagai dependensi pengembangan dengan
harness = false. - Fungsi
black_boxmencegah kompiler menghitung atau menghapus pekerjaan yang diukur, dan pengukuran struktural seperti jumlah pemanggilan menjelaskan jurang kinerja sebelum waktu diukur. cargo fuzzmenjalankan libFuzzer terbimbing cakupan pada toolchain nightly, menyimpan masukan pemantik panic sebagai artefak, dan artefak itu menjadi bahan pengujian regresi.- Siklus generator, fungsi sasaran, dan pengawas dapat ditiru dengan pustaka standar secara deterministik, dengan kehilangan pembimbing cakupan sebagai batasnya.
Referensi#
- Criterion.rs — User Guide — Cara kerja pengukuran, statistik, dan struktur berkas benchmark Criterion.
- criterion — docs.rs
— API
bench_function,iter, danblack_boxbeserta contohnya. - cargo fuzz — GitHub
— Subcommand, struktur direktori
fuzz/, dan cara menjalankan target sasaran. - libFuzzer — LLVM Compiler Infrastructure — Mesin fuzzing terbimbing cakupan yang dipakai cargo fuzz.