注意: 最新版のドキュメントをご覧ください。この第1版ドキュメントは古くなっており、最新情報が反映されていません。リンク先のドキュメントが現在の Rust の最新のドキュメントです。
Rustはコードのパフォーマンスをテストできるベンチマークテストをサポートしています。
早速、 src/lib.rc
を以下のように作っていきましょう(コメントは省略しています):
#![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 bench
が cargo test
よりも時間がかかることにお気づきになったかもしれません。
これは、Rustがベンチマークをかなりの回数繰り返し実行し、その結果の平均を取るためです。
今回のコードでは非常に小さな処理しか行っていないために、 1 ns/iter (+/- 0)
という結果を得ました、
しかし、この結果は変動することがあるでしょう。
以下は、ベンチマークを書くときのアドバイスです:
iter
の外に移し、計測したい箇所のみを iter
の中に書きましょう。iter
ループを短く高速にしましょう、そうすることでベンチマークの実行は高速になり、キャリブレータは実行の長さをより良い精度で補正できるようになります。iter
ループ中のコードの処理を簡潔にしましょう。ベンチマークを書くときに気をつけなければならないその他の点は: 最適化を有効にしてコンパイルしたベンチマークは劇的に最適化され、 もはや本来ベンチマークしたかったコードとは異なるという点です。 たとえば、コンパイラは幾つかの計算がなにも外部に影響を及ぼさないことを認識してそれらの計算を取り除くかもしれません。
#![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
の呼出を以下のようにすることで可能です:
b.iter(|| { // `;` が無いことに注意して下さい (明示的な `return` を使うこともできます)。 (0..1000).fold(0, |old, new| old ^ new) });
もう一つの方法としては、ジェネリックな test::black_box
関数を呼び出すという手段が有ります、
test::black_box
関数はオプティマイザにとって不透明な「ブラックボックス」であり、
オプティマイザに引数のどれもが利用されていると考えさせることができます。
#![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
しかしながら、上のどちらかの方法をとったとしても依然オプティマイザはテストケースを望まない形で変更する場合があります。