Rust: ベンチマークの計測
概要
Table of Contents
計測方法
cargo bench
を使う方法
cargo bench
はベンチマーク用にマークされた関数を実行する Cargo コマンドである。ベンチマークの記述方法は単体テストの記述と似ていてシンプルである。
以下の例は 7[msec] 以上かかる処理として実装した a_time_consuming_process()
と、3[msec] 以上かかる処理として実装した another_time_consuming_process()
の 2 つの関数のベンチマークを計測している。
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_one(b: &mut Bencher) {
b.iter(|| a_time_consuming_process());
}
#[bench]
fn bench_another(b: &mut Bencher) {
b.iter(|| another_time_consuming_process());
}
fn a_time_consuming_process() {
use std::{thread, time};
let ten_millis = time::Duration::from_millis(7);
thread::sleep(ten_millis);
}
fn another_time_consuming_process() {
use std::{thread, time};
let ten_millis = time::Duration::from_millis(3);
thread::sleep(ten_millis);
}
実行結果を見るとそれぞれのベンチマークは 1 回のイテレーション (≒ ベンチマーク対象の 1 回の実行) あたり 8.003[msec] と 3.584[msec] かかっていることが報告されている。1 イテレーションの平均実行時間だけでなくその標準偏差 (±68% 信頼区間相当) も報告してくれるのは go test -bench
よりも地味に嬉しいところである。
% cargo +nightly bench
Compiling cargo-bench v0.1.0 (/Users/.../cargo-bench)
Finished bench [optimized] target(s) in 0.73s
Running unittests (target/release/deps/cargo_bench-84738c004ff48c65)
running 2 tests
test bench_one ... bench: 8,003,734 ns/iter (+/- 746,325)
test bench_another ... bench: 3,583,931 ns/iter (+/- 339,532)
test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out; finished in 10.36s
cargo bench
にコマンドラインオプションを指定することで特定のベンチマークのみを実行対象としたり標準出力への出力を省略しないようにできる。
今のところ、この cargo bench
を使う方法は unstable であることに注意しなければならない。実行は +nightly
を強制されるため、ベンチマーク対象の依存ライブラリが nightly
ビルドに対応していなければ諦めるしかない。また #![feature(test)]
はクレート全体に影響する lib.rs
か main.rs
の先頭に記述しなければならず、したがって production 開発とは別のクレートとしてベンチマークを計測するしかない。
bencher::iter()
を使う方法
前述の cargo bench
は単体テストのように記述していたが、bench::iter()
を使うことでより詳細なベンチマーク結果を取得することができる。ベンチマーク結果を確認するだけではなく、数値として CSV ファイルに出力したりレポートを作成するような場合はこの方法が便利である。
#![feature(test)]
extern crate test;
use test::bench::iter;
fn main() {
let result = iter(&mut move || a_time_consuming_process());
println!("{:#?}", result);
}
fn a_time_consuming_process() {
use std::{thread, time};
let ten_millis = time::Duration::from_millis(7);
thread::sleep(ten_millis);
}
簡単なコマンドラインアプリなので cargo run
で実行できるが、正しい計測結果を得るためにリリースビルドの --release
オプションを忘れないように注意。また cargo bench
と同様に +night
も必要である。
% cargo +nightly run --release
Compiling bench-iter v0.1.0 (/Users/.../bench-iter)
Finished release [optimized] target(s) in 0.35s
Running `target/release/bench-iter`
Summary {
sum: 405928943.155,
min: 7773840.275,
max: 8460736.91,
mean: 8118578.8631,
median: 8121248.5,
var: 40289251615.482544,
std_dev: 200721.82645512806,
std_dev_pct: 2.4723763831060994,
median_abs_dev: 203069.64636000013,
median_abs_dev_pct: 2.5004732506338176,
quartiles: (
7992062.675,
8121248.5,
8259680.875,
),
iqr: 267618.2000000002,
}
標準偏差 std_dev
が分かるので (計測値が正規分布に従うと想定できれば) 68–95–99.7則に従って 68%, 95%, 99.7% 信頼区間を算出することができる。
この方法は cargo bench
よりも詳細で柔軟であるが、依然として +nightly
かつ #![feature(test)]
を強制されることから production 開発とは別のクレートで開発するしかない。
criterion
を使う方法
計測対象の依存ライブラリが nightly
ビルドに対応していない、production リポジトリの中でベンチマークを計測したいという場合は stable ビルドでベンチマーク計測ができる Criterion.rs を使用することができる。
benches/
ディレクトリの下にベンチマークを実行するファイルを作成する (後述の cargo-criterion
ではファイル名が Rust の識別子として使用されることに注意)。
// benches/my_bench.rs
use criterion::{Criterion, criterion_group, criterion_main};
fn bench_one(c: &mut Criterion) {
c.bench_function("Is this 7msec?", |b| b.iter(|| a_time_consuming_process()));
}
fn bench_another(c: &mut Criterion) {
c.bench_function("Is this 3msec?", |b| b.iter(|| another_time_consuming_process()));
}
criterion_group!(benches, bench_one, bench_another);
criterion_main!(benches);
fn a_time_consuming_process() {
use std::{thread, time};
let ten_millis = time::Duration::from_millis(7);
thread::sleep(ten_millis);
}
fn another_time_consuming_process() {
use std::{thread, time};
let ten_millis = time::Duration::from_millis(3);
thread::sleep(ten_millis);
}
Cargo.toml にも追加の記述が必要。name
には benches/
の下に作成した計測実行用の Rust ファイルを指定する。
[package]
name = "criterion"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[dev-dependencies]
criterion = "0.3"
[[bench]]
name = "my_bench"
harness = false
実行は cargo bench
だけで良い点に注目。
% cargo bench
Compiling criterion v0.1.0 (/Users/.../criterion)
Finished bench [optimized] target(s) in 2.49s
Running unittests (target/release/deps/criterion-72781684356afadf)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests (target/release/deps/my_bench-1b6a0c581e7b096a)
WARNING: HTML report generation will become a non-default optional feature in Criterion.rs 0.4.0.
This feature is being moved to cargo-criterion (https://github.com/bheisler/cargo-criterion) and will be optional in
a future version of Criterion.rs. To silence this warning, either switch to cargo-criterion or enable the 'html_reports'
feature in your Cargo.toml.
Gnuplot not found, using plotters backend
Is this 7msec? time: [7.9545 ms 8.0138 ms 8.0720 ms]
change: [+0.4788% +1.6583% +2.8679%] (p = 0.01 < 0.05)
Change within noise threshold.
Is this 3msec? time: [3.5390 ms 3.5609 ms 3.5819 ms]
change: [-0.0401% +0.9278% +1.9928%] (p = 0.08 > 0.05)
No change in performance detected.
Found 3 outliers among 100 measurements (3.00%)
3 (3.00%) low mild
すぐに消えるメッセージをよく見るとウォーミングアップ (予備実行) を行っていることがわかる。また初回実行には出力されない "Change within noise threshold." や "No change in performance detected." といったメッセージから察するに、直前に実行したベンチマーク計測をどこかに記録していて有意な差が出ているかを報告しているらしい。
WARNING メッセージが報告しているように cargo-criterion
をインストールすると target/criterion
の下に詳細な HTML レポートが出力されるようになる。ただしグラフ描画のバックエンドに gnuplot を起動しようとするので予めインストールしておく必要がある。
% cargo install cargo-criterion
% cargo criterion
ベンチマークに有用な機能
black_box()
関数
test::black_box()
は引数と同じ値を返す恒等関数。テストやベンチマークにおいて Rust コンパイラによる最適化を可能な限り阻止することを目的としている。
例えば高度な最適化を行うコンパイラは、固定値を引数に取る冪等関数 (同じ引数に対して必ず同じ結果を返す関数) を見つけると、関数呼び出し命令の代わりにその計算結果そのものをバイナリに埋め込んでしまうことがある。このようなベンチマークは意味がないため、固定値を black_box()
化することでコンパイラから "隠す" ことを目的としている。
fn fibonacci(n: u64) -> u64 {
match n {
0 => 1,
1 => 1,
n => fibonacci(n-1) + fibonacci(n-2),
}
}
#[bench]
fn bench_fibonacci(b: &mut Bencher) {
b.iter(|| fibonacci(black_box(38)));
};