Perlでベンチマーク

Perl Advent Calendar 2014の枠が空いていたので、ただのメモですが10日目の記事として晒すことに。


今さらかなり基本的なことだけど、Perlでのベンチマークの実行方法を調べて、適当にいろいろ試してみたメモ。

Benchmarkモジュール

基本的にはperldocを見れば良い。 ゆとりなのでPerldoc.jpにて。

いろいろ関数があるけども、よく使いそうな雰囲気なのは次の2つな気がした。

  • timethis : 特定のコードの実行速度を測る
  • cmpthese : 複数のコードの実行速度を測りつつ比較する

timethis

DateTimeは遅いって言われているけど、実際にどれくらい遅いのか試しに測ってみる。 現在の時間を取得して1日足すという処理を例に。

timethisの1つ目の引数は、2つ目の引数の処理を実行する回数を示している。 第2引数には、CodeRefかevalしたい文字列を指定する。 正の数を指定した場合は「実行回数」を、負の数を指定した場合は「最低実行時間」を示している。 0はデフォルト値で、-3を指定したときと同じ挙動を示す。

use Benchmark;
use DateTimeX::Factory;

my $dt = DateTimeX::Factory->new(time_zone => 'Asia/Tokyo');

timethis 0, sub {
    $dt->now->add(days => 1);
};

実際に実行してみた結果が次の出力。 CPU時間3秒くらい使って9331回実行して、毎秒約3千回の速度で実行できたらしい。

timethis for 3:  3 wallclock secs ( 3.13 usr +  0.00 sys =  3.13 CPU) @ 2981.15/s (n=9331)

cmpthese

これだけだと、この処理にかかる時間がわかるだけで比較ができないし、実行環境によって速度も変わってくる。 ので、cmptheseを使って比較を行ってみる。

cmptheseでは、1つ目の引数はtimethisと同じで、2つ目の引数にはHashRefにて実行したいコードと、そのコードの名前を渡してあげる。

use Benchmark qw/ cmpthese /;
use DateTimeX::Factory;
use Time::Moment;

my $dt = DateTimeX::Factory->new(time_zone => 'Asia/Tokyo');

cmpthese 0, {
    'DateTime'     => sub { $dt->now->add(days => 1) },
    'Time::Moment' => sub { Time::Moment->now->plus_days(1) },
};

実際に実行してみた結果が次の出力。 もはや、DateTimeが遅いとかじゃなくてTime::Moment速すぎワロタ状態。

Rateの項目が実行速度を示していて、それより右側は他との比較を示している。 この例だと、Time::Momentと名付けた処理は、DateTimeと名付けた処理よりも42690%速い、すなわち428倍の実行速度があることがわかる。 逆に、DateTimeTime::Momentよりも100%遅い(差が大きすぎて丸められてしまった!)、ことがわかる。 ちなみに、時間系モジュールの速度比較はここで詳しく行っている(Time::Momentまじ速い)。

                  Rate     DateTime Time::Moment
DateTime        3034/s           --        -100%
Time::Moment 1298354/s       42690%           --

適当にいろいろ測ってみる

ちらっとどこかで見かけた関数呼び出しのオーバーヘッドを測ってみる

このコードでお試し。

sub one   { Time::Moment->now->plus_days(1) }
sub two   { one() }
sub three { two() }

cmpthese 0, {
    one   => \&one,
    two   => \&two,
    three => \&three,
};

oneに元の処理を記述し、two, threeでは無駄に関数呼び出しを挟んでいる。 上記のTime::Momentの処理を実行した場合、元の処理がかなり速いため、関数呼び出しのオーバーヘッドが如実に表れている。 自分の環境だと140000[1/sec] = 7[μsec]くらいが関数呼び出しにかかる時間らしい。

           Rate three   two   one
three 1047791/s    --  -12%  -22%
two   1191842/s   14%    --  -11%
one   1338232/s   28%   12%    --

次に、上記のDateTimeの処理で置き換えた版を実行してみる。

my $dt = DateTimeX::Factory->new(time_zone => 'Asia/Tokyo');

sub one   { $dt->now->add(days => 1) }
sub two   { one() }
sub three { two() }

cmpthese 0, {
    one   => \&one,
    two   => \&two,
    three => \&three,
};

当たり前だが、今度はどれも誤差レベルのしか生まれなかった。 関数の中身自体が遅く、呼び出し回数が少ない場合には、関数呼び出しのオーバーヘッドはさほど問題では無くなる。

        Rate   one three   two
one   2753/s    --   -2%   -3%
three 2810/s    2%    --   -1%
two   2825/s    3%    1%    --

シュワルツ変換によるソート速度向上を測定してみる

配列の各要素のsha512の16進数表現を文字列としてソートする場合を作って試した。 (そんな場合が実際にあるかはわからないが、単に重そうな処理をかませたかっただけ)

それぞれのラベルでは次の処理を行う

  • normal : 愚直にsortブロック内でsha512_hexを実行する
  • schwartz : シュワルツ変換で頑張る
use Benchmark qw/ cmpthese /;
use Digest::SHA qw/ sha512_hex /;

my @array = (1..100);

cmpthese 0, {
    normal => sub {
        my @sorted = sort {sha512_hex($a) cmp sha512_hex($b)} @array;
    },
    schwartz => sub {
        my @sorted =
            map  {$_->[1]}
            sort {$a->[0] cmp $b->[0]}
            map  {[sha512_hex($_), $_]}
            @array;
    },
};

この場合、シュワルツ変換を使った方が約7倍速くなった。

           Rate   normal schwartz
normal    305/s       --     -86%
schwartz 2186/s     617%       --

ソート対象の配列の要素数を100個から10個に減らした場合でも、まだまだシュワルツ変換を使った方が3倍速く、有効そう。

            Rate   normal schwartz
normal    7590/s       --     -67%
schwartz 23057/s     204%       --

こんな感じで

実行速度が実際にどれくらいなのか、AとBの実装ではどれくらい速度差があるのか、、、などを知りたいときは、ちゃんとベンチマークを実行し、数字ベースで比較できると良いな、という感想。