ベンチマークテスト

Rustはコードのパフォーマンスをテストできるベンチマークテストをサポートしています。 早速、 src/lib.rc を以下のように作っていきましょう(コメントは省略しています):

#![feature(test)] fn main() { extern crate test; pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; use test::Bencher; #[test] fn it_works() { assert_eq!(4, add_two(2)); } #[bench] fn bench_add_two(b: &mut Bencher) { b.iter(|| add_two(2)); } } }
#![feature(test)]

extern crate test;

pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;
    use test::Bencher;

    #[test]
    fn it_works() {
        assert_eq!(4, add_two(2));
    }

    #[bench]
    fn bench_add_two(b: &mut Bencher) {
        b.iter(|| add_two(2));
    }
}

不安定なベンチマークのフィーチャを有効にするため、 test フィーチャゲートを利用していることに注意して下さい。

ベンチマークテストのサポートを含んだ test クレートをインポートしています。 また、 bench アトリビュートのついた新しい関数を定義しています。 引数を取らない通常のテストとは異なり、ベンチマークテストは &mut Bencher を引数に取ります。 Bencher はベンチマークしたいコードを含んだクロージャを引数に取る iter メソッドを提供しています。

ベンチマークテストは以下のように cargo bench のようにして実施できます:

$ cargo bench
   Compiling adder v0.0.1 (file:///home/steve/tmp/adder)
     Running target/release/adder-91b3e234d4ed382a

running 2 tests
test tests::it_works ... ignored
test tests::bench_add_two ... bench:         1 ns/iter (+/- 0)

test result: ok. 0 passed; 0 failed; 1 ignored; 1 measured

ベンチマークでないテストは無視されます。 cargo benchcargo test よりも時間がかかることにお気づきになったかもしれません。 これは、Rustがベンチマークをかなりの回数繰り返し実行し、その結果の平均を取るためです。 今回のコードでは非常に小さな処理しか行っていないために、 1 ns/iter (+/- 0) という結果を得ました、 しかし、この結果は変動することがあるでしょう。

以下は、ベンチマークを書くときのアドバイスです:

注意点: 最適化

ベンチマークを書くときに気をつけなければならないその他の点は: 最適化を有効にしてコンパイルしたベンチマークは劇的に最適化され、 もはや本来ベンチマークしたかったコードとは異なるという点です。 たとえば、コンパイラは幾つかの計算がなにも外部に影響を及ぼさないことを認識してそれらの計算を取り除くかもしれません。

#![feature(test)] fn main() { extern crate test; use test::Bencher; #[bench] fn bench_xor_1000_ints(b: &mut Bencher) { b.iter(|| { (0..1000).fold(0, |old, new| old ^ new); }); } }
#![feature(test)]

extern crate test;
use test::Bencher;

#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
    b.iter(|| {
        (0..1000).fold(0, |old, new| old ^ new);
    });
}

このベンチマークは以下の様な結果となります

running 1 test
test bench_xor_1000_ints ... bench:         0 ns/iter (+/- 0)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured

ベンチマークランナーはこの問題を避ける2つの手段を提供します。 iter メソッドが受け取るクロージャは任意の値を返すことができ、 オプティマイザに計算の結果が利用されていると考えさせ、その計算を取り除くことができないと保証することができます。 これは、上のコードにおいて b.iter の呼出を以下のようにすることで可能です:

fn main() { struct X; impl X { fn iter<T, F>(&self, _: F) where F: FnMut() -> T {} } let b = X; b.iter(|| { // note lack of `;` (could also use an explicit `return`). // `;` が無いことに注意して下さい (明示的な `return` を使うこともできます)。 (0..1000).fold(0, |old, new| old ^ new) }); }
b.iter(|| {
    // `;` が無いことに注意して下さい (明示的な `return` を使うこともできます)。
    (0..1000).fold(0, |old, new| old ^ new)
});

もう一つの方法としては、ジェネリックな test::black_box 関数を呼び出すという手段が有ります、 test::black_box 関数はオプティマイザにとって不透明な「ブラックボックス」であり、 オプティマイザに引数のどれもが利用されていると考えさせることができます。

#![feature(test)] extern crate test; fn main() { struct X; impl X { fn iter<T, F>(&self, _: F) where F: FnMut() -> T {} } let b = X; b.iter(|| { let n = test::black_box(1000); (0..n).fold(0, |a, b| a ^ b) }) }
#![feature(test)]

extern crate test;

b.iter(|| {
    let n = test::black_box(1000);

    (0..n).fold(0, |a, b| a ^ b)
})

2つの手段のどちらも値を読んだり変更したりせず、小さな値に対して非常に低コストです。 大きな値は、オーバーヘッドを減らすために間接的に渡すことができます(例: black_box(&huge_struct))。

上記のどちらかの変更を施すことでベンチマークの結果は以下のようになります

running 1 test
test bench_xor_1000_ints ... bench:       131 ns/iter (+/- 3)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured

しかしながら、上のどちらかの方法をとったとしても依然オプティマイザはテストケースを望まない形で変更する場合があります。