プロセスを並列に立ち上げて負荷を掛けるようなベンチマークを実行することって、よくありますよね。(例 : クエリキャッシュを切ったほうがいイカ? ベンチマークしてみた - 酒日記 はてな支店)
Perl で Parallel::ForkManager を使うとそういう処理も簡単に書けて便利なのですが、何度も同じようなコードを書くうちに、これもうちょっと抽象化したら使いやすいかも、と思って Parallel::Benchmark というモジュールを書いてみました。
リポジトリはこちらです。 https://github.com/fujiwara/p5-Parallel-Benchmark
たとえばフィボナッチ数 fib(10) を求めるベンチマーク。
use Parallel::Benchmark; sub fib { my $n = shift; return $n if $n == 0 or $n == 1; return fib( $n - 1 ) + fib( $n - 2 ); } my $bm = Parallel::Benchmark->new( benchmark => sub { my ($self, $id) = @_; fib(10); # code for benchmarking return 1; # score }, concurrency => 2, debug => 1, ); my $result = $bm->run();
実行結果。
2012-02-20T14:40:21 [INFO] starting benchmark: concurrency: 2, time: 3 2012-02-20T14:40:21 [DEBUG] spwan child 1 pid 7029 2012-02-20T14:40:21 [DEBUG] spwan child 2 pid 7030 2012-02-20T14:40:22 [DEBUG] starting benchmark on child 1 pid 7029 2012-02-20T14:40:22 [DEBUG] starting benchmark on child 2 pid 7030 2012-02-20T14:40:25 [DEBUG] done benchmark on child 1: score 46618, elapsed 3.000 sec. 2012-02-20T14:40:25 [DEBUG] done benchmark on child 2: score 47316, elapsed 3.000 sec. 2012-02-20T14:40:25 [INFO] done benchmark: score 93934, elapsed 3.000 sec = 31306.742 / sec
ベンチマークしたい処理を coderef にして渡してやることで、指定した並列度 (concurrency) で子プロセスを fork() し、一定時間(デフォルト 3秒) 実行した結果をまとめて表示してくれます。経過時間は CPU time ではなく実時間です。
もうちょっと実用的な例。MongoDB に対して並列度 1, 2, 4, 8 で書き込み処理のベンチマーク。
setup, teardown を指定すると、子プロセス起動後、ベンチマークの前後に実行する処理を指定できます。
use Parallel::Benchmark; use MongoDB::Connection; my $bm = Parallel::Benchmark->new( setup => sub { my $self = shift; $self->stash->{mongo} = MongoDB::Connection->new; }, benchmark => sub { my $self = shift; my $id = shift; my $mongo = $self->stash->{mongo}; my $collection = $mongo->test->foo; $collection->save({ id => $id }); 1; }, teardown => sub { my $self = shift; delete $self->stash->{mongo}; }, time => 1, ); for my $c ( 1, 2, 4, 8 ) { $bm->concurrency($c); $bm->run; }
2012-02-20T14:34:04 [INFO] starting benchmark: concurrency: 1, time: 1 2012-02-20T14:34:06 [INFO] done benchmark: score 9816, elapsed 1.002 sec = 9799.106 / sec 2012-02-20T14:34:06 [INFO] starting benchmark: concurrency: 2, time: 1 2012-02-20T14:34:08 [INFO] done benchmark: score 24317, elapsed 0.999 sec = 24343.461 / sec 2012-02-20T14:34:08 [INFO] starting benchmark: concurrency: 4, time: 1 2012-02-20T14:34:10 [INFO] done benchmark: score 27234, elapsed 1.002 sec = 27171.831 / sec 2012-02-20T14:34:10 [INFO] starting benchmark: concurrency: 8, time: 1 2012-02-20T14:34:12 [INFO] done benchmark: score 36331, elapsed 1.001 sec = 36308.339 / sec
子プロセスから $self->stash に値をセットすることで、その値をベンチマーク終了後、親プロセス側で参照することもできます。
benchmark => sub { my ($self, $id) = @_; my $res = $ua->get("http://127.0.0.1/"); $self->stash->{code}->{ $res->code }++; # status code を保存しておく return 1; },
# $result = $bm->run(); { 'score' => 1886, 'elapsed' => '3.0022655' 'stashes' => { '1' => { # $id ごとの stash がみえる 'code' => { '200' => 932, '500' => 7 } }, '2' => { 'code' => { '200' => 935, '500' => 12 } } }, }
他にも、
- Ctrl-C で止めた場合でも、実行中のベンチマーク結果を途中まででちゃんと集計してくれるとか
- 始めたはいいけど設定時間長すぎた!でもここで止めたらそれまでの時間が無駄になるから我慢……みたいな場面ありますよね
- 子プロセスの fork() がすべて終わって、1秒後に (シグナルを使って) 計測開始するとか
- 並列度が大きくても、開始と終了のタイミングが結構ちゃんと揃います
など、細かいところで工夫しているので、よろしければ使ってみてください。
それでは Happy Benchmarking!