Parallel::Benchmark というモジュールを書きました

プロセスを並列に立ち上げて負荷を掛けるようなベンチマークを実行することって、よくありますよね。(例 : クエリキャッシュを切ったほうがいイカ? ベンチマークしてみた - 酒日記 はてな支店)

PerlParallel::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!