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:

  • Google Gemini updates: Flash 1.5, Gemma 2 and Project Astra
  • Displaying External Posts on Your al-folio Blog
  • a post with plotly.js
  • a post with image galleries
  • a post with tabs