Rustでプログラムのベンチマークを行うにはどうすればよいですか? たとえば、プログラムの実行時間を秒単位で取得するにはどうすればよいですか?
9 に答える
2 年後 (このページに出くわした将来の Rust プログラマーを助けるために)、テスト スイートの一部として Rust コードのベンチマークを行うツールが存在することは注目に値するかもしれません。
(以下のガイド リンクから)#[bench]
属性を使用すると、標準の Rust ツールを使用して、コード内のメソッドのベンチマークを行うことができます。
extern crate test;
use test::Bencher;
#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
b.iter(|| {
// Use `test::black_box` to prevent compiler optimizations from disregarding
// Unused values
test::black_box(range(0u, 1000).fold(0, |old, new| old ^ new));
});
}
コマンドの場合、cargo bench
これは次のようなものを出力します。
running 1 test
test bench_xor_1000_ints ... bench: 375 ns/iter (+/- 148)
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured
リンク:
サードパーティの依存関係を追加せずに時間を測定するには、次を使用できますstd::time::Instant
。
fn main() {
use std::time::Instant;
let now = Instant::now();
// Code block to measure.
{
my_function_to_measure();
}
let elapsed = now.elapsed();
println!("Elapsed: {:.2?}", elapsed);
}
単純にコードの時間を測定したい場合は、time
クレートを使用できます。ただし、その間は非推奨です。フォローアップ クレートはchrono
.
に追加time = "*"
しますCargo.toml
。
追加
extern crate time;
use time::PreciseTime;
メイン関数の前に
let start = PreciseTime::now();
// whatever you want to do
let end = PreciseTime::now();
println!("{} seconds for whatever you did.", start.to(end));
完全な例
貨物.toml
[package]
name = "hello_world" # the name of the package
version = "0.0.1" # the current version, obeying semver
authors = [ "you@example.com" ]
[[bin]]
name = "rust"
path = "rust.rs"
[dependencies]
rand = "*" # Or a specific version
time = "*"
錆.rs
extern crate rand;
extern crate time;
use rand::Rng;
use time::PreciseTime;
fn main() {
// Creates an array of 10000000 random integers in the range 0 - 1000000000
//let mut array: [i32; 10000000] = [0; 10000000];
let n = 10000000;
let mut array = Vec::new();
// Fill the array
let mut rng = rand::thread_rng();
for _ in 0..n {
//array[i] = rng.gen::<i32>();
array.push(rng.gen::<i32>());
}
// Sort
let start = PreciseTime::now();
array.sort();
let end = PreciseTime::now();
println!("{} seconds for sorting {} integers.", start.to(end), n);
}
Rust プログラムをベンチマークする方法はいくつかあります。ほとんどの実際のベンチマークでは、適切なベンチマーク フレームワークを使用する必要があります。これは、台無しになりやすいいくつかのこと (統計分析を含む) に役立つためです。一番下の「ベンチマークを書くのが難しい理由」セクションもお読みください。
すばやく簡単:標準ライブラリInstant
からDuration
コードの実行時間をすばやく確認するには、 の型を使用できますstd::time
。モジュールはかなり最小限ですが、単純な時間測定には問題ありません。前者は単調に増加するクロックであり、後者はそうではないため、Instant
代わりに使用する必要があります。SystemTime
例 (遊び場):
use std::time::Instant;
let before = Instant::now();
workload();
println!("Elapsed time: {:.2?}", before.elapsed());
std の基礎となるプラットフォーム固有の実装は、ドキュメントでInstant
指定されています。要するに、現在(そしておそらく永遠に)、プラットフォームが提供できる最高の精度(またはそれに非常に近いもの)を使用していると想定できます。私の測定と経験から、これは通常約 20 ns です。
std::time
で十分な機能が提供されていない場合は、 を参照してくださいchrono
。ただし、期間を測定するために、その外部クレートが必要になることはまずありません。
ベンチマーク フレームワークの使用
フレームワークを使用することは、よくある間違いを防ごうとするため、多くの場合良い考えです。
Rust の組み込みベンチマーク フレームワーク (夜間のみ)
Rust には便利なベンチマーク機能が組み込まれていますが、残念ながら 2019 年 7 月の時点ではまだ不安定です。関数に属性を追加して、1 つの引数#[bench]
を受け入れるようにする必要があります。&mut test::Bencher
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_workload(b: &mut Bencher) {
b.iter(|| workload());
}
実行cargo bench
すると、次のように出力されます。
running 1 test
test bench_workload ... bench: 78,534 ns/iter (+/- 3,606)
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out
基準
クレートcriterion
は安定版で動作するフレームワークですが、組み込みのソリューションよりも少し複雑です。より高度な統計分析を行い、より豊富な API を提供し、より多くの情報を生成し、プロットを自動的に生成することさえできます。
Criterion の使用方法の詳細については、「クイックスタート」セクションを参照してください。
ベンチマークの作成が難しい理由
ベンチマークを作成する際には、多くの落とし穴があります。1 つの間違いで、ベンチマークの結果が無意味になる可能性があります。以下は、重要だが忘れられがちなポイントのリストです。
最適化でコンパイル:
rustc -O3
またはcargo build --release
. でベンチマークを実行するとcargo bench
、Cargo は自動的に最適化を有効にします。最適化された Rust コードと最適化されていない Rust コードの間には大きなパフォーマンスの違いがあることが多いため、このステップは重要です。ワークロードを繰り返す: ワークロードを 1 回実行するだけでは、ほとんどの場合役に立ちません。タイミングに影響を与える可能性があるものは多数あります。システム全体の負荷、オペレーティング システムの動作、CPU スロットリング、ファイル システム キャッシュなどです。したがって、ワークロードをできるだけ頻繁に繰り返します。たとえば、Criterion はすべてのベンチマークを少なくとも 5 秒間実行します (ワークロードが数ナノ秒しかかからない場合でも)。測定されたすべての時間は、標準ツールである平均および標準偏差を使用して分析できます。
ベンチマークが完全に削除されていないことを確認してください。ベンチマークは本質的に非常に人工的です。通常、所要時間のみを測定するため、ワークロードの結果は検査されません。ただし、これは、優れたオプティマイザーは、(時間の経過は別として) 副作用がないため、ベンチマーク全体を削除できることを意味します。したがって、オプティマイザーをだますには、結果の値を何らかの方法で使用して、ワークロードが削除されないようにする必要があります。簡単な方法は、結果を印刷することです。より良い解決策は次のようなもの
black_box
です。この関数は基本的に、LLVM が値で何が起こるかを知ることができないという点で、LLVMから値を隠します。何も起こりませんが、LLVM は知りません。そこが肝心だ。優れたベンチマーク フレームワークは、いくつかの状況でブロック ボックスを使用します。たとえば、
iter
メソッドに指定されたクロージャー (組み込みと Criterion の両方Bencher
) は値を返すことができます。その値は自動的に に渡されますblack_box
。定数値に注意してください: 上記の点と同様に、ベンチマークで定数値を指定すると、オプティマイザーがその値専用のコードを生成する場合があります。極端な場合、ワークロード全体が 1 つの定数に折りたたまれ、ベンチマークが役に立たなくなる可能性があります。
black_box
LLVM が過度に積極的に最適化するのを避けるために、すべての定数値を渡します。測定のオーバーヘッドに注意してください。期間の測定には時間がかかります。通常は数十ナノ秒ですが、測定時間に影響を与える可能性があります。したがって、数十ナノ秒よりも高速なすべてのワークロードについて、各実行時間を個別に測定するべきではありません。ワークロードを 100 回実行し、100 回すべての実行にかかった時間を測定できます。それを 100 で割ると、平均 1 回の時間が得られます。前述のベンチマーク フレームワークもこのトリックを使用しています。Criterion には、副作用のある非常に短いワークロード (何かを変更するなど) を測定する方法もいくつかあります。
他にも多くのことがあります。残念ながら、すべての問題をここにリストすることはできません。本格的なベンチマークを書きたい場合は、オンライン リソースをさらに読んでください。
この答えは時代遅れです!クレートは、ベンチマークに関して
time
何の利点も提供しません。std::time
最新の情報については、以下の回答を参照してください。
タイムクレートを使用して、プログラム内の個々のコンポーネントのタイミングを試すことができます。
実装言語に関係なく、プログラムの実行時間を調べる簡単な方法はtime prog
、コマンド ラインで実行することです。例えば:
~$ time sleep 4
real 0m4.002s
user 0m0.000s
sys 0m0.000s
最も興味深い測定値は、通常user
、システムで何が起こっているかに関係なく、プログラムによって実行された実際の作業量を測定する です (sleep
ベンチマークするのはかなり退屈なプログラムです)。real
実際に経過した時間をsys
測定し、プログラムに代わって OS によって実行された作業量を測定します。
現在、次の Linux 機能へのインターフェイスはありません。
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts)
getrusage
times
(マンページ:man 2 times
)
Linux で Rust プログラムの CPU 時間とホットスポットを測定するために利用できる方法は次のとおりです。
/usr/bin/time program
perf stat program
perf record --freq 100000 program; perf report
valgrind --tool=callgrind program; kcachegrind callgrind.out.*
との出力はperf report
、valgrind
プログラム内のデバッグ情報が利用できるかどうかによって異なります。動作しない場合があります。
実行時間を測定するもう 1 つの解決策は、構造体などのカスタム型を作成し、そのためのDrop
特性を実装することです。
例えば:
struct Elapsed(&'static str, std::time::SystemTime);
impl Drop for Elapsed {
fn drop(&mut self) {
println!(
"operation {} finished for {} ms",
self.0,
self.1.elapsed().unwrap_or_default().as_millis()
);
}
}
impl Elapsed {
pub fn start(op: &'static str) -> Elapsed {
let now = std::time::SystemTime::now();
Elapsed(op, now)
}
}
そして、それをいくつかの機能で使用します:
fn some_heavy_work() {
let _exec_time = Elapsed::start("some_heavy_work_fn");
// Here's some code.
}
関数が終了すると、drop メソッド_exec_time
が呼び出され、メッセージが出力されます。