Rust: ベンチマークの計測

Takami Torao Rust 1.53 #Rust
  • このエントリーをはてなブックマークに追加

概要

Table of Contents

  1. 概要
  2. 計測方法
    1. cargo bench を使う方法
    2. bencher::iter() を使う方法
    3. criterion を使う方法
  3. ベンチマークに有用な機能
    1. black_box() 関数

計測方法

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.rsmain.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)));
};
F