Teknologi

benchfmt.rs: Benchmark Rust dengan Format benchstat Go

benchfmt.rs: Benchmark Rust dengan Format benchstat Go

Bartek Płotka merilis benchfmt.rs, runner benchmark Rust tanpa dependensi yang menghasilkan keluaran kompatibel dengan benchstat milik Go. Alat itu menambahkan proyeksi /key=value, pencatatan alokasi heap dalam satuan B/op dan allocs/op, serta integrasi pprof, dengan flag CLI yang sama seperti go test -bench. Pengumumannya terbit 3 November 2026, dan pengujiannya memakai dua beban kerja nyata: decoder varint 36 baris dan decoder payload Prometheus Remote Write 2.0.

TL;DR

  • benchfmt.rs mengisi celah yang lama dikeluhkan: format standar untuk membandingkan benchmark Rust secara A/B, lengkap dengan penilaian reliabilitas seperti benchstat.
  • Konverter Criterion dan libtest sudah disertakan, jadi proyek Rust yang sudah punya benchmark tidak perlu menulis ulang dari nol.
  • Pencatatan alokasi per operasi memakai B/op dan allocs/op, satuan yang akrab bagi pengguna Go tapi jarang tersedia rapi di ekosistem Rust.
  • Integrasi pprof memungkinkan jawaban atas pertanyaan dari mana overhead berasal, bukan cuma berapa lama prosesnya.
  • Fixture varint berukuran 4.798 byte berisi 2.000 nilai uint64, sementara batch Remote Write 2.0 dengan 2.000 series menyusut dari 244,9 KiB menjadi 57,0 KiB setelah kompresi Snappy.

Apa masalah yang diselesaikan benchfmt.rs?

Masalahnya bukan kurang alat benchmark, tapi kurang format bersama. Rust sudah punya Criterion dan libtest, dan keduanya berjalan baik. Yang tidak ada adalah format standar untuk membandingkan dua implementasi secara langsung, pengukuran alokasi heap yang seragam, dukungan pprof, serta skor reliabilitas yang mudah dipahami. Tanpa empat hal itu, perbandingan A/B cenderung berakhir dengan dua angka mentah yang dibandingkan manual.

Płotka menulis sendiri bahwa ia datang ke Rust setelah bertahun-tahun menulis Go untuk sistem yang sensitif performa, termasuk Prometheus dan Thanos. Kebiasaan itu membentuk ekspektasinya: benchmark seharusnya bisa dijalankan dengan satu perintah, dan hasilnya bisa dibandingkan dengan alat statistik yang sudah dipercaya banyak orang. benchfmt.rs adalah jawaban atas ekspektasi tersebut.

Pendekatan yang diambil sengaja konservatif. Selain runner aslinya, ia menyertakan konverter yang membaca keluaran Criterion dan libtest lalu menerjemahkannya ke format benchfmt milik Go. Bagi yang belum percaya pada runtime benchmark baru, jalur konverter membuat alat ini bisa dipakai tanpa mengganti apa pun di proyek yang sudah berjalan.

Bagaimana bentuk beban uji yang dipakai?

Dua beban kerja dipilih karena mewakili dua jenis biaya berbeda. Yang pertama, DecodeVarints, adalah decoder aliran varint LEB128 sepanjang 36 baris yang menulis ke slice berukuran 2.000 elemen milik pemanggil, tanpa alokasi heap. Fixture-nya bernama varints_2000.bin, berukuran 4.798 byte, dan memuat 2.000 nilai uint64 yang mencampur indeks simbol satu byte, offset bucket dua byte, delta empat byte, serta timestamp milidetik enam byte.

Yang kedua, DecodeRemoteWrite2, adalah decoder badan HTTP penuh untuk payload Remote Write 2.0 yang dikompresi Snappy. Pengujian memakai batch 200 dan 2.000 series, dan angka 2.000 dipilih karena sama dengan nilai default max_samples_per_send di Prometheus. Fixture 2.000 series mencampur counter, gauge, histogram klasik dan native, 1.000 exemplar trace, serta metadata metrik.

Detail formatnya penting untuk memahami angkanya. Remote Write 2.0 mengintern semua string ke tabel simbol di level atas dan menyimpan label_refs dalam bentuk varint terpacking per series. Efeknya terlihat langsung pada ukuran: batch 2.000 series menjadi 244,9 KiB tanpa kompresi dan 57,0 KiB setelah Snappy. Rasio itu menjelaskan kenapa decoder seperti ini layak dioptimasi, karena biayanya muncul di jalur panas setiap pengiriman metrik.

KebutuhanEkosistem GoEkosistem Rust sebelum benchfmt.rs
Runner benchmarkgo test -benchcargo bench dengan Criterion atau libtest
Perbandingan statistikbenchstat dengan skor reliabilitasTidak ada format standar, dibandingkan manual
Alokasi per operasiB/op dan allocs/op bawaanTersebar, tidak seragam antar alat
Profilinggo tool pprofPerlu integrasi terpisah
Proyeksi hasilDukungan /key=valueTidak ada padanan langsung

Kenapa alokasi heap lebih penting daripada waktu eksekusi?

Karena alokasi menentukan biaya yang tidak terlihat di stopwatch. Dua fungsi bisa punya durasi sama, tapi yang satu mengalokasi puluhan ribu objek per operasi dan yang lain nol. Yang pertama membebani allocator, memicu variasi latensi, dan memperburuk perilaku di bawah beban tinggi. Angka B/op dan allocs/op membuat perbedaan itu terlihat, dan itulah yang ditambahkan benchfmt.rs lewat penghitung alokasi thread-local.

Untuk decoder, target idealnya jelas: nol alokasi per operasi. DecodeVarints mencapai itu karena menulis ke slice yang sudah dialokasi pemanggil. DecodeRemoteWrite2 tidak bisa sepenuhnya menghindari alokasi karena payload membawa struktur dinamis, tapi jumlahnya bisa ditekan dengan penggunaan ulang buffer.

Kebiasaan yang perlu dibuang adalah mengoptimasi durasi lebih dulu. Kalau alokasi masih tinggi, durasi akan tetap bervariasi meski rata-ratanya turun. Menekan alokasi biasanya menurunkan rata-rata sekaligus ekor distribusinya, dan itu yang terasa di produksi.

Kapan sebaiknya memakai konverter, bukan runner baru?

Pakai konverter saat proyek sudah punya benchmark Criterion atau libtest yang berjalan dan hasilnya dipercaya. Mengganti runner di tengah jalan menambah variabel baru, dan setiap variabel baru membuat perbandingan sebelum-sesudah jadi tidak sah. Konverter mempertahankan beban uji yang sama, hanya mengubah format keluaran, sehingga hasil lama dan baru masih bisa dibandingkan.

Pakai runner asli saat mulai dari nol, atau saat butuh kontrol lebih atas fase pemanasan dan pengukuran. Runner benchfmt.rs memakai flag yang sama dengan go test -bench, jadi perpindahannya tidak menuntut pembelajaran ulang. Untuk tim yang anggotanya berpindah antara Go dan Rust, kesamaan antarmuka ini menghemat waktu.

Yang tidak boleh dilakukan adalah mencampur angka dari dua metode dalam satu tabel perbandingan. Kalau sebagian hasil datang dari konverter dan sebagian dari runner asli, tulis sumbernya di tabel. Tanpa itu, pembaca akan menyimpulkan selisih performa yang sebenarnya hanya selisih metodologi.

Apakah hasilnya membuktikan Rust lebih cepat dari Go?

Untuk beban kerja yang diuji, ya, Rust lebih cepat, dan penulisnya menyatakan itu terus terang. Tapi kesimpulan itu berlaku untuk dua decoder spesifik, bukan untuk semua program. Beban kerja yang hasilnya bergantung pada alokasi string, I/O, atau lapisan framework bisa memberi urutan berbeda.

Yang lebih berharga dari artikel itu bukan verdict bahasa mana yang menang, tapi alur kerjanya. Alur tersebut terdiri dari: tetapkan fixture realistis, jalankan benchmark di kedua bahasa, catat waktu dan alokasi, lalu buktikan lewat profiler dari mana selisihnya datang. Alur ini bisa dipakai untuk membandingkan dua library dalam bahasa yang sama, dan di situ manfaatnya paling besar untuk tim sehari-hari.

Kode dan keluaran mentah benchmark tersedia di repositori bwplotka/benchmarks, penjelasan lengkap ada di artikel aslinya, dokumentasi Criterion ada di Criterion Book, dan alat pembanding Go bisa dibaca di dokumentasi benchstat.

Bagaimana menghindari kesimpulan palsu dari perbandingan benchmark?

Pertama, jangan membandingkan hasil dari mesin berbeda. Selisih antar mesin biasanya lebih besar daripada selisih antar implementasi, sehingga perbandingan menjadi tidak bermakna. Kalau harus memakai mesin berbeda, ukur ulang implementasi pembanding di mesin yang sama.

Kedua, jangan menyimpulkan dari satu beban kerja. Artikel aslinya memakai dua beban kerja karena satu beban kerja tidak cukup untuk menyimpulkan karakter sebuah bahasa. Untuk keputusan teknis, minimal tiga beban kerja dengan karakter berbeda: komputasi ketat, alokasi berat, dan I/O.

Ketiga, laporkan ketidakpastian. Angka tanpa rentang variasi menyiratkan presisi yang tidak dimiliki pengukuran. Menyertakan sebaran hasil bukan tanda kelemahan, melainkan tanda bahwa pengukurannya dilakukan dengan benar.

Dari mana sebaiknya mulai kalau repo belum punya benchmark?

Mulai dari satu fungsi yang paling sering muncul di profil aplikasi, bukan dari yang paling mudah diuji. Fungsi itu biasanya jelas dari profiler atau dari log latensi. Ukur fungsi tersebut, simpan hasilnya, lalu jadikan pembanding untuk perubahan berikutnya. Satu benchmark yang relevan lebih berharga daripada dua puluh benchmark yang tidak pernah dipakai mengambil keputusan.

Kedua, tetapkan fixture dari data produksi yang sudah dianonimkan. Data sintetis cenderung terlalu mudah, dan angka yang dihasilkan membuat optimasi yang salah terlihat berhasil. Ketiga, jalankan benchmark di mesin yang tidak dipakai bersamaan dengan pekerjaan lain, karena laptop dengan puluhan tab terbuka bukan tempat mengukur selisih lima persen.

Bagaimana memilih antara mengoptimasi kode dan mengganti alat?

Pilih mengoptimasi kode selama profil menunjukkan biaya terbesar ada di logika aplikasi sendiri. Kalau profiler menunjukkan waktu habis di allocator atau di serialisasi library, mengganti atau menyetel alat lebih masuk akal. Urutan ini penting karena optimasi kode di atas alat yang lambat hanya memindahkan biaya, bukan menghilangkannya.

Untuk memutuskan, pakai profil sebagai dasar, bukan dugaan. Satu sesi profiling pada beban kerja nyata biasanya cukup untuk melihat apakah waktu terbesar ada di kode sendiri atau di lapisan bawah. Tanpa profil, diskusi tentang optimasi hanya berisi selera.

Yang perlu dihindari adalah mengoptimasi tanpa baseline. Perubahan yang tidak diukur sebelum dan sesudahnya tidak bisa diklaim sebagai perbaikan, sekalipun kodenya terlihat lebih rapi.

FAQ

Apa itu benchfmt.rs?

Runner benchmark Rust tanpa dependensi yang mengeluarkan format kompatibel benchstat, dilengkapi konverter dari Criterion dan libtest, pencatatan alokasi heap, serta integrasi pprof. Dijelaskan Bartek Płotka pada 3 November 2026.

Berapa ukuran fixture yang dipakai dalam pengujian?

Fixture varint bernama varints_2000.bin berukuran 4.798 byte dan memuat 2.000 nilai uint64. Fixture Remote Write 2.0 memakai batch 2.000 series yang menjadi 244,9 KiB tanpa kompresi dan 57,0 KiB setelah Snappy.

Apakah harus mengganti Criterion?

Tidak. Konverter membaca keluaran Criterion dan menerjemahkannya ke format benchfmt, jadi benchmark yang sudah ada tetap berjalan. Runner asli hanya diperlukan kalau ingin kontrol penuh atas fase pemanasan dan pengukuran.

Kenapa angka B/op penting?

Karena waktu eksekusi tidak menangkap biaya alokasi. Fungsi dengan durasi sama bisa berbeda jauh dalam jumlah alokasi per operasi, dan perbedaan itu memengaruhi latensi di bawah beban serta tekanan pada allocator.

Apakah ini bisa dipakai untuk membandingkan dua library Rust?

Bisa, dan itu pemakaian yang paling praktis. Selama kedua library diuji dengan fixture dan lingkungan yang sama, format keluaran benchfmt memungkinkan perbandingan statistik yang sama seperti membandingkan dua implementasi Go.

💬 Komentar (0)

Belum ada komentar. Jadilah yang pertama! 💬

Komentar yang memuat tautan, bahasa asing, atau aksara non-Latin ditahan dulu buat ditinjau.