benchmarking in rust
Giriş
Rust yazmayı çok sevmemizin en önemli sebeplerinden biri performansı. Safety özelliklerinden dolayı aslında yazarken sadece derlenip derlenmediğine bakıyoruz ve C/C++’a yakın bir performans bekliyoruz. Her ne kadar çoğu şeyi derleyici bizim için halletse de biz programcılara da büyük bi iş düşüyor. Peki bu uğraşımızın gerçekten işe yaramadığını nasıl anlayabiliriz? Tabii ki benchmark yaparak. Peki bu işler nasıl yürüyor hadi biraz bakalım.
En basit çözüm
Tabii ki hepimizin ilk yaptığı bir timer koyup geçen süreye bakmak.
let start = std::time::Instant::now();
// Çok çok uzun kod parçası
println!("Geçen süre: ", start.elapsed());
Bu yöntem birçok noktada işe yarıyor ama ya çok küçük bir parçayı optimize etmek istiyorsak? O zaman biraz daha modern yöntemlere geçmemiz geliyor.
Rust’ın orijinal benchmark aracı
Aslında Rust’ın içinde kendi orijinal bir benchmark aracı var. cargo bench ile çalıştırılabilmesi gereken bir sistem bu. Ancak yıllardır sadece nightly sürümde kullanılabiliyor. Bu yüzden kararlı sürümde yardımcı bir crate kullanmak zorundayız.
Ancak öncesinde nightly açıkken nasıl bench yapabiliriz onu da gösterelim.
#![feature(test)]
// Test feature'ını etkinleştirmek gerekiyor. Bu sadece nightly'de mevcut.
/// Projedeki herhangi bir Rust dosyasına aşağıdaki şekilde eklenebilir.
/// Recursive Fibonacci fonksiyonu
#[inline]
pub fn fibonacci_recursive(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2),
}
}
#[cfg(test)]
mod tests {
use super::*;
extern crate test; // Required: test is a special compiler crate
use test::Bencher;
#[bench]
fn bench_fibonacci_recursive(b: &mut Bencher) {
b.iter(|| {
test::black_box(fibonacci_recursive(20))
});
}
}
Bunu cargo +nightly bench komutuyla (nighly’de olduğunu belirterek) çalıştırabiliriz. Çıktısı da şu şekilde oluyor:
running 2 tests
test tests::bench_fibonacci_iterative ... bench: 0.30 ns/iter (+/- 0.02)
test tests::bench_fibonacci_recursive ... bench: 0.31 ns/iter (+/- 0.04)
test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out; finished in 0.63s
Criterion
Criterion, Rust’ın kendi benchmark özelliği henüz kararlı sürüme gelmediği için (belki de bu yüzden) elimizdeki kozumuz oluyor. Criterion birçok özelliğiyle Rust’ta defacto olarak benchmark standardı olduğu için muhtemelen resmi aracın gelmesine hiç ihtiyacımız yok.
Cargo.toml içine bağımlılıkları ekle
Peki bunu kullanabilmek için ne yapmamız geliyor. Öncelikle projemizin Cargo.toml dosyasına aşağıdaki satırları ekliyoruz.
[dev-dependencies]
criterion = "0.8"
[[bench]]
name = "fibonacci_bench"
harness = false
Bu development dependency olarak criterion’u projeye ekliyor. Ek olarak fibonacci_bench adında bir benchmark tanımlıyor. Buradaki harness = false ayarıyla standart benchmark harness’ini iptal ediyoruz ki Criterion kendisininkini kullansın.
Test edilecek fonksiyon
lib.rs içine aşağıdaki fonksiyonumuzu test etmek amacıyla ekliyoruz.
/// Fibonacci recursion (yavaş)
pub fn fibonacci_recursive(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2),
}
}
Benchmark’ı ekle
benches/fibonacci_bench.rs dosyasını oluşturuyoruz ve aşağıdaki şekilde ilk benchmarkımızı yazıyoruz.
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use benchmarking::fibonacci_recursive;
fn bench_fibonacci(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci_recursive(black_box(20))));
}
criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
Şimdi burayı biraz inceleyelim zira dikkat etmemiz gereken birkaç nokta var. Öncelikle black_box fonksiyonu ile derleyicinin kodumuzu gereksiz yere optimize etmesini engelliyoruz ki böylece kodumu gerçek bir senaryoda nasıl çalışıyor test edebilelim. Bu fonksiyon derleyiciye “bu değeri herhangi bir şey olabilirmiş gibi varsay ve sakın optimize etme” mesajını verir. Böylece derleyici o değer sabitmiş gibi önceden hesaplamaları yapmaz.
-
"fib 20"— raporda görünen isim -
|b|— iterasyon döngüsüne kontrol eden Bencher -
b.iter(...)— closure’ı defalarca çağırır ve her seferinde süreyi ölçer -
|| fibonacci(...)— her iterasyonda çalışan fonksiyon
criterion_group!(benches, bench_fibonacci); macrosu benches isminde benchmark grubu oluşturur.
Son olarak criterion_main!(benches); satırı benches grubunu main olarak açar ve asıl executable buradan oluşmuş olur.
Çalıştırma
cargo bench --bench fibonacci_bench komutunu kullanarak benchmarımızı çalıştırıyoruz. Şöyle bir çıktı bizi karşılıyor:
fib 20 time: [15.459 µs 16.348 µs 17.643 µs]
Found 7 outliers among 100 measurements (7.00%)
2 (2.00%) high mild
5 (5.00%) high severe
Yeni bir fonksiyon
Şimdi daha hızlı bir implementasyon yapalım ve tekrardan benchmark sonuçlarımıza bakalım. Aşağıdaki fonksiyon lib.rs içerisine ekleyelim.
/// Fibonacci iterative (hızlı)
pub fn fibonacci_iterative(n: u64) -> u64 {
if n == 0 {
return 0;
}
let mut a = 0;
let mut b = 1;
for _ in 1..n {
let temp = a + b;
a = b;
b = temp;
}
b
}
Daha sonra benchmark dosyasındaki fonksiyonumuzu fibonacci_iterative olacak şekilde değiştirip benchmarkı tekrardan çalıştırıyoruz.
fib 20 time: [6.1208 ns 6.4793 ns 6.9362 ns]
change: [−99.963% −99.958% −99.951%] (p = 0.00 < 0.05)
Performance has improved.
Found 10 outliers among 100 measurements (10.00%)
4 (4.00%) high mild
6 (6.00%) high severe
Görüldüğü üzere performansımız %99 oranında iyileşmiş.
Sonuç
Sonuç olarak Criterion kullanarak benchmarkları nasıl yapabileceğimizi öğrendik. Burada asıl önemli nokta her zamanki gibi darboğazları bulup onları çözmek. Gereğinden fazla zaman harcamamak gerekiyor çünkü en hızlı kod hiç yazılmamış koddur, ikinci en hızlı kod ise nereyi optimize edeceğinizi bildiğiniz koddur!
Enjoy Reading This Article?
Here are some more articles you might like to read next: