例えばCSVなファイルを読み込んで、それをハッシュの中に展開、格納と言った感じの動作をPerlで行いたかったとします。例えば
1,2,3
と言った行を
my %hash = (
'col1' => 1,
'col2' => 2,
'col3' => 3
);
のようなハッシュに展開する関数が欲しいわけです。皆さんはこれをどういう風に実装しますか?ぱっと思いつくのはforループですよね
my @colums = ('a', 'b', 'c');
my @values = (1, 2, 3); # もちろん実際にはsplit(/,/, $line)とか、CSVパーサーを使う
my %h;
for (0..$#columns) {
$h{ $columns[$_] } = $values[$_];
}
実はこのようなCっぽい書き方はPerlでは大概遅いです。DWIMな言語なので、その辺りはPerl内のオプティマイザに任せるようなコードを書いたほうが俄然性能が良くなるのです。そこで僕は今まで自分の中でHash Magicと呼んでいるものを使ってました。これは、%hashとして定義した物に対して、複数の値を一度に挿入する事ができるものです。
my @colums = ('a', 'b', 'c');
my @values = (1, 2, 3);
my %h;
@h{ @columns } = @values;
%h から @hの辺りがややこしいかもしれませんが、これがハッシュに対して複数キーを一度に設定する時の書き方です。これはforループに比べると遥かに高速に動きますし、なにより@columnsの中身を一カ所変えるだけで勝手にカラム名の追加/削除/変更に対応してくれるのが嬉しい限りの書き方です。
ですが、どうやらベンチマークを取ってみると妙な事になってきました。
さて、ここからは実際のベンチマークコードをみてもらいましょう。まずは基本のベンチマークコードです。
sub run_bench
{
my %args = @_;
my @columns = @{ $args{columns} };
my @values = @{ $args{values} };
cmpthese(500_000, {
hash_magic => sub {
my %h;
@h{ @columns } = @values;
},
forloop1 => sub {
my %h;
for (0..$#columns) {
$h{ $columns[$_] } = $values[$_];
}
},
forloop2 => sub {
my %h;
my $count = 0;
$h{ $columns[$count++] } = $_ for @values;
},
manual => sub {
my %h;
$h{a} = $values[0];
$h{b} = $values[1];
$h{c} = $values[2];
$h{d} = $values[3];
$h{e} = $values[4];
},
automanual => do {
my $code = "sub {\nmy \%h;\n";
for my $i (0..$#columns) {
my $col = $columns[$i];
$code .= "\$h{'$col'} = \$values[$i];\n";
}
$code .= "}\n";
eval $code;
},
});
}
ご覧のように2パターンのforループ、hash magic、そして手動でカラム名→カラム番号を紐付けるルーチンを用意してます。最後のautomanualは後ほど解説します。
これをまず、キーの文字列が1文字('a', 'b', 'c')と言った1文字だけの場合のベンチを取ってみます。すると結果はこんな感じになりました:
Rate forloop1 forloop2 automanual manual hash_magic
forloop1 233645/s -- -20% -58% -59% -62%
forloop2 290698/s 24% -- -48% -49% -52%
automanual 561798/s 140% 93% -- -1% -8%
manual 568182/s 143% 95% 1% -- -7%
hash_magic 609756/s 161% 110% 9% 7% --
ぬはっhash_magic超速ス!とか思ってエントリを書きかけたのですが、その後カラムキーの長さを約30文字に変更してみたのです。そうしたらなんと結果が違ってきました:
Rate forloop1 forloop2 hash_magic automanual manual
forloop1 201613/s -- -17% -54% -64% -64%
forloop2 243902/s 21% -- -44% -56% -57%
hash_magic 438596/s 118% 80% -- -21% -22%
automanual 555556/s 176% 128% 27% -- -1%
manual 561798/s 179% 130% 28% 1% --
今度は手動のほうが速い!ちなみにこの差はだいたいキーの長さが5文字くらいになったところで出始めます。先ほどのrun_bench()を使用してキーの文字列長を変えて行くベンチマーク用コードはこんな感じです:
use strict;
use warnings;
use Benchmark qw(cmpthese);
{ # Benchmark short keys
my @columns = ('a'..'e');
my @values = (1..5);
run_bench(columns => \@columns, values => \@values);
}
{ # Benchmark medium keys
my @columns = map { "col-$_" } (1..5);
my @values = (1..5);
run_bench(columns => \@columns, values => \@values);
}
{ # Benchmark long keys
my @columns = map { "col-abcdefghijklmnopqrstuvwxyz-$_" } (1..5);
my @values = (1..5);
run_bench(columns => \@columns, values => \@values);
}
どうも推測するに長い文字列の入った配列を@hに代入する時にその取り出し分のコストのほうが上回ってくるということみたいです。だから固定文字列として指定した'manual'のほうがパフォーマンス的に上回ってくるわけですね。
元々hash magicを使おうと思ったのは、最終的な動作時までカラムの名前や数が分からない場合でも動くものが作りたかったからなんですが、これを見てくるとどうにかその辺をうまくできないかと思いだしたわけです。それが上記run_bench()中のautomanualという項目で指定されている関数です。
この項目、何をやっているかというと、動的に与えられたカラム名を明示指定した関数を作成してます。@columnsの中身によって関数の定義自体を文字列として作成し、evalしてるわけです。これで一応「動的」に「手動/明示指定」な関数が作れる訳です。
でも醜い。美しくない。そもそもこのコード自体がものすごく遅ければ問題ですが、正直それほど効果あるのかなぁ、という疑問も抱いています。要はこのようなトリックを使う事によって起こる可読性の損失と、実際のパフォーマンスの向上のバランスの問題ですね。
今回はこのコードが2000万回ほど呼ばれる予定なので多少差は出ると予測されるので多分使用しますが、僕は元々「いらん最適化は最適化しない場合と比べて改善どころか改悪になる」というスタンスの人間ですし、こういった、Perlビルトインなものに対してのベンチマークというのは結構その結果を鵜呑みするわけにもいかないので正直ちょっと悩みどころです。
まぁ多分もとの@h{@cols} = @valueと言った意図をコメントか何かに残しておいて、その上で最適化する方法を取ると思います。
コメント