まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2006/07/29(土)Perlの迷信

迷信というか勘違いなのか、PerlのVersionがあがって最適化されたのか。Perl 5.8にて確認。

foreach(0..n) vs for(my $i=0; $i<n+1; $i++)

前者は (0..n) の配列が作られるのでその分遅くなるだろうと思っていました。後述しますが、keys(%HASH) は配列生成が発生するので必ずしも効率がよくないという話を読んだりもしていたので。ところが実際に計測してみると……

use strict;
use Benchmark;
my %test;
my $count = 10000;
$test{test1} = sub {
	my $x; foreach(my $i=0; $i<100; $i++) { $x+=$i; }
};
$test{test2} = sub {
	my $x; foreach(0..99) { $x+=$_; }
};
$test{test3} = sub {
	my $x; foreach my $i (0..99) { $x+=$i; }
};
timethese($count, \%test);

実行結果

test1:  3 wallclock secs ( 3.13 usr +  0.00 sys =  3.13 CPU) @ 3194.89/s (n=10000)
test2:  3 wallclock secs ( 2.35 usr +  0.00 sys =  2.35 CPU) @ 4255.32/s (n=10000)
test3:  2 wallclock secs ( 2.44 usr +  0.00 sys =  2.44 CPU) @ 4098.36/s (n=10000)

ループ変数を指定したとしても、foreach の方が速いです。多分 perl の最適化が効いてるんでしょうねぇ……。固定数ではなく、配列に対してforeach(0..$#ary)としても結果は一緒でした。

keys(%hash) は遅い?

ハッシュのすべてのkeyからなる配列を作る keys は遅いといわれ、実際perlのマニュアルにも遅いかもしれないとかかれています。ですから

foreach(keys(%ENV)) { }

ではなく

while(my($k,$v)=each(%ENV)) { }

とした方が効率がいいとばかり思っていました。使う側としてはkeys(%HASH)の方が楽なのですが、eachを使うように心がけていました。ところが、次のようにベンチマークをとってみると、

$test{test1} = sub {
	my $x;
	while(my ($k,$v) = each(%ENV)) { if ($v) { $x++; } }
};

$test{test2} = sub {
	my $x;
	foreach(keys(%ENV)) { if ($ENV{$_}) { $x++; } }
};
timethese($count, \%test);

実行結果

test1:  2 wallclock secs ( 2.25 usr +  0.00 sys =  2.25 CPU) @ 4444.44/s (n=10000)
test2:  2 wallclock secs ( 1.51 usr +  0.00 sys =  1.51 CPU) @ 6622.52/s (n=10000)

これまた非常に悲しい結果に。$ENV は23個ぐらいしか定義されてないので、要素数の多いハッシュならどうなるか試してみました。

my %h;
foreach(0..999) {
	$h{crypt($_, '00')} = $_;
}

として任意数のハッシュを生成します。実行結果

●10要素
test1:  1 wallclock secs ( 0.95 usr +  0.00 sys =  0.95 CPU) @ 10526.32/s (n=10000)
test2:  1 wallclock secs ( 0.66 usr +  0.00 sys =  0.66 CPU) @ 15151.52/s (n=10000)
●100要素
test1:  9 wallclock secs ( 8.77 usr +  0.00 sys =  8.77 CPU) @ 1140.25/s (n=10000)
test2:  6 wallclock secs ( 5.73 usr +  0.00 sys =  5.73 CPU) @ 1745.20/s (n=10000)
●1000要素(計測ループ数1000回)
test1:  9 wallclock secs ( 8.85 usr +  0.00 sys =  8.85 CPU) @ 112.99/s (n=1000)
test2:  7 wallclock secs ( 6.72 usr +  0.00 sys =  6.72 CPU) @ 148.81/s (n=1000)
●10000要素(計測ループ数100回)
test1: 10 wallclock secs (10.09 usr +  0.00 sys = 10.09 CPU) @  9.91/s (n=100)
test2:  8 wallclock secs ( 8.62 usr +  0.00 sys =  8.62 CPU) @ 11.60/s (n=100)
●100000要素(計測ループ数10回)
test1: 10 wallclock secs (10.27 usr +  0.00 sys = 10.27 CPU) @  0.97/s (n=10)
test2:  9 wallclock secs ( 8.99 usr +  0.02 sys =  9.01 CPU) @  1.11/s (n=10)

10万要素でも逆転する様子を見せません(汗) 通常10万要素なんてあり得ないわけで、keysを積極的に使うべしという結論になりました。

結論

どちらも、わざわざ速くするために書きにくい方を選んで、結果的に遅くしてたという情けない結果になりました(汗) んー、こういう基礎研究的なことは悩んだときにきちんとやった方がいいんですねぇ(汗*1

それと、アセンブラ好き好き(謎)な人間には、インタプリタの実行速度は若干奇妙に映ります。まあ、こんなもんなんでしょうけど(笑

*1 : 実際問題として、これらを全部修正したところで全体で1ms速くなるかといわれたら疑問ですけど、でもなんか悔しい(笑)