毎秒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
それと、アセンブラ好き好き(謎)な人間には、インタプリタの実行速度は若干奇妙に映ります。まあ、こんなもんなんでしょうけど(笑
2006/07/28(金)マニュアルから知らなかった関数
perlの関数メモ
知らなかった関数から抜粋。もっと早く読んでおけばよかったなぁ。
lc/uc
最初が小文字変換、後者が大文字変換。要するに次と等価。
$x=lc($x) : $x =~ tr/A-Z/a-z/; $x=uc($x) : $x =~ tr/a-z/A-Z/;
study SCALAR
何回も文字列に対するパターンマッチを行なうアプリケーションで、そのような文字列 SCALAR を予め学習しておきます。(中略)別のスカラを study した場合には、以前に学習した内容は「忘却」されてしまいます。
(この study の仕組みは、まず、検索される文字列内のすべての文字のリンクされたリストが作られ、たとえば、すべての 'k' がどこにあるかがわかるようになります。各おのの検索文字列から、C プログラムや英語のテキストから作られた、頻度の統計情報に基づいて、もっともめずらしい文字が選ばれます。 この「めずらしい」文字を含む場所だけが調べられるのです。)
日本語の場合……難しいところですが、同じ(短めの)文字列に対して予めリンクリストを作っておけば、次からのパターンマッチが高速化されるようです。実際には計測して速いかどうか確認してから使えと書いてあります。
ループ関連
redo
redo コマンドは、条件を再評価しないで、ループブロックの始めからもう一度実行を開始します。 continue ブロックがあっても、実行されません
ラベル指定 last/next/redo
last などのコマンドは、ラベルによって抜けるループを指定できるようです。多段ループから抜けたいときいつも
my $z; foreach my $x (1..9) { my $flag; foreach my $y (1..9) { if ($x*$y>50) { $z=$x*$y; $flag=1; last; } } if ($flag) { last; } }
と書いてたのですが、ラベル指定を使えば
my $z; OUT_LOOP: foreach my $x (1..9) { foreach my $y (1..9) { if ($x*$y>50) { $z=$x*$y; last OUT_LOOP; } } }
と書けばよかったようです。
正規表現/パターンマッチ
$&
$cookie_val =~ s/(\W)/ '%' . unpack('H2', $1)/eg;
とかやって URI エンコードをするのは有名ですか、わざわざ $1 のフレーズホルダーを使わなくても、
$cookie_val =~ s/\W/ '%' . unpack('H2', $&)/eg;
とすればよかったようです。$&というのはマッチした文字列そのものを示す特殊変数で、マッチ部より手前と後ろを示す $`, $' を使えば、
$cookie_val == "$`$&$'"
はマッチングが成功すれば常に成り立つことを意味してます。
注:$&は正規表現の動作速度を低下させる恐れがあります。URIエンコード程度ならば$1を使いましょう(コメントでの指摘ありがとうございま)。→参考
m//;
m//; はマッチングをとる演算子で、何も付けずに//;したときと一緒。つまりは
if ($str =~ /<(\w+)>(.*?)<\/\w+>/) { print "$1 = $2\n"; }
としたときは暗黙に m が指定されているということです。/ がセパレーターなので\/エスケープしなければなりません。しかし置換表現ならば、
$str =~ s#<(\w+)>(.*?)</\w+>#print "$1=$2\n"#eg; $str =~ s|<(\w+)>(.*?)</\w+>|print "$1=$2\n"|eg; $str =~ s!<(\w+)>(.*?)</\w+>!print "$1=$2\n"!eg; $str =~ s[<(\w+)>(.*?)</\w+>][print "$1=$2\n"]eg; $str =~ s{<(\w+)>(.*?)</\w+>}{print "$1=$2\n"}eg;
などとセパレーターを変更することで、/をエスケープする必要がないわけです。でもマッチングを取るときは
if ($str =~ |<(\w+)>(.*?)<\/\w+>|) { print "$1 = $2\n"; }
とかできないで不便だなぁと思ってたのですが、つまり次みたいにすればよかったようです。
if ($str =~ m|<(\w+)>(.*?)<\/\w+>|) { print "$1 = $2\n"; }
q//;
q//; は正規表現をコンパイルした状態で保存する演算子です。正規表現をあたかもオブジェクトのように扱えます。/o オプションを使えば正規表現をコンパイルして保持できるのですが、2度とその場所の正規表現を変更できなくなるので使えませんでした。*1
@urlsを正規表現が格納された配列、@aryがマッチング処理をする配列だとします。
my $match; LABEL: foreach my $line (@ary) { foreach(@urls) { if ($line =~ /$_/) { $match=1; last LABEL; } } }
とすると、中のif文を実行する度に正規表現がコンパイルされ、@ary が大きいときに大変なロスとなります。
# 先にコンパイルしておく foreach(@urls) { $_ = qr/$_/; } my $match; LABEL: foreach my $line (@ary) { foreach(@urls) { if ($line =~ /$_/) { $match=1; last LABEL; } } }
とすることで、正規表現のコンパイルが1度のみとなり大きな速度向上が見込めます。ちなみにこのときのコンパイルされた正規表現式はref($str)に対して'Regexp'を返します。
my $reg = qr#<(\w+)>(.*?)</\w+>#; print "$reg\n", ref($reg), "\n";
実行結果
(?-xism:<(\w+)>(.*?)</\w+>) Regexp
pos SCALAR
対象の変数に対して、前回の m//g が終了した場所のオフセットを返します。また、代入することでオフセットを変えることも可能です。
と書かれています。/g 付きの正規表現は、連続マッチングを取るオプションですが、前回マッチングを取った場所より後ろが次のマッチング開始位置になります。無限ループなどにならない、至極当然の方式なのですが、極希にこの仕様が困ったことになります。
my $str = <<TEXT; >> 1 << >> 2 << >> 3 << TEXT
という文字列から「>>のみの行で始まり<<のみの行で終わる」ブロックを抽出することを考えます。こののみの行ってのが厄介です。
$str = "\n$str"; # 前処理*2 $str =~ s/\n>>\n(.*?)\n<<\n/print "$1 "; "\n"/esg;
とすれば、うまく行きそうに見えます*3。ですが実際には、
1 3
と見えて表示されません。というのも、1つめのブロックとマッチする最後の"\n"と、2つめのブロックをマッチするときの最初の"\n"が同じものを示しているためです。ここで pos 関数の出番です。
while($str =~ m|\n>>\n(.*?)\n<<\n|g) { print "$1(pos:", pos ($str), ") "; pos($str) = length($`); $str = $` . "\n" . $'; }
とすれば、
1(pos:9) 2(pos:9) 3(pos:10)
となり正しく認識出来ます。
追記:もっとスマートな方法
前回マッチした部分とマッチする "\G" という要素を使えば、もっとスマートな方法で実現可能でした。
my $str = "aaa0bbb0ccc0ddd1eee0fff0ggg"; $str =~ s/(?:\G|0)(\w\w\w)0/ print "$1\n"; /eg;
実行結果は、
aaa [0] bbb [4] ccc [8] fff [19]
また、(?:...|...)は$1などへの割り当てない部分正規表現で、マッチ部の割り当てという無駄な処理が減るので処理が効率化できます*4。また\Gを使うことで ^\w\w\w とマッチする効果があり、前処理として\nを追加する必要がなくなります。
.......ソース書きなおそ(笑
2006/07/28(金)map関数の使い方
perl の map 関数の使い方
map関数の説明は他を参照してもらうとして。
$categories->[n] = { cat_ID => カテゴリID, cat_name => カテゴリ名 }; $post2cat->[n] = { post_id => 記事ID, category_id => カテゴリID };
という2つのハッシュリファレンスからなる配列があったとき、記事ID→カテゴリ名という変換テーブル(ハッシュ)を作る方法。
my %cat2name = map { $_->{cat_ID} => $_->{cat_name} } @categories; my %post2name = map { $_->{post_id} => $cat2name{ $_->{category_id} } } @post2cat;
とすると、$post2name{記事ID} → カテゴリ名となります。でもこの場合、@post2cat の最後に出た要素が優先されるので、最初に出た要素を優先するために、
map { $post2name{ $_->{post_id} } ||= $cat2name{ $_->{category_id} } } @post2cat;
と書けます。
foreach(@ary) で回してるものは、mapに置き換えた方がスマートかつ効率的かも知れもません。
grep
grepは別に正規表現なだけではなく、格納条件を指定することもできるらしい。
my @newary = grep { $_ > 100 } @ary;
は
my @newary; foreach(@ary) { if ($_ > 100) { push(@newary, @ary); } }
と等価らしい。grep コマンドの印象が強かったから全然気づかなかった。
追記
んー便利だ。今まで foreach(@ary) で書いてたところが、いくつも書き直せそうだけど……まぁいいや。*1
とりあえず、まだ知らないことがありそうなのでPerl5の関数リストでも眺めておこう。
2006/07/26(水)FileHandle vs Symbol
perl でオブジェクト指向を目指し、use strictしたプログラムを徹底していくと、どうにかしたくなるのが「ファイルハンドル」の存在です。
open(FD, "test.txt"); close(FD);
この FD をオブジェクトとして使い関数に対して引数として与えたりしたいのですが、use strict な環境では
my $fh = 'FD'; open($fh, "test.txt"); close($fh);
とやっても、エラーになってしまいます。かと言ってこのためだけに no strict refs; ともしたくない。またこの方法では、Perlをマルチスレッド動作させるとき、ファイルディスクリプタの名前空間が衝突し、ファイルが開けなくなる問題もあります。
ネットで情報を漁っていると、こういうときはファイルハンドルを動的生成する方法が紹介されています。
use FileHandle (); my $fh = FileHandle->new(); open($fh, "test.txt"); close($fh);
use IO::File (); my $fh = IO::File->new(); open($fh, "test.txt"); close($fh);
いずれの方法も実際うまく動きますし、動作上はなんの問題もありません。ただ、これらのモジュールはファイルハンドルを生成するためだけに使うにはリッチすぎます。つまり機能的すぎてモジュールのロードが遅いということです。*1
たた単純に、ファイルハンドルを動的生成したい場合は、
use Symbol (); my $fh = Symbol::gensym(); open($fh, "test.txt"); close($fh);
とするのがベストだと思われます。
どれくらい速度が違うか、それぞれのサンプルだけのプログラムを作成し実行し、time コマンドで計測してみましたので参考までに。
実現方法 | 実行速度 |
---|---|
FileHandle | 100ms |
IO::File | 94ms |
Symbol | 23ms |
直接指定 | 14ms |
こういう計測の結果、adiary では Symbol モジュールを使っています。
Perl 5.6.0以降限定、もっとよい方法
Perl 5.6.0からの新機能として、ファイルハンドルを自動で生成する機能があります。
ハンドラを使用する関数 (open(), opendir(), pipe(), socketpair(), sysopen(), socket(), accept()) では、ファイルハンドルとして未定義のスカラ変数が与えられたとき、ファイルやディレクトリのハンドルを自動的に生成し変数に設定します。
と書かれています(意訳)。つまりどういうことかというと、上と同等のことをやるためには、
open(my $fh, "test.txt"); close($fh);
と書けば十分だということになります。こうすれば Symbol モジュールのロードは必要なくなり、メモリや実行時間の節約になります。ただし、これが実行できるのは5.6.0以降ですので、ソースの最初に
use 5.6.0;
と書いておくべきでしょう。adiaryでは現在(β7以降)この方法を使用しています。
2006/07/19(水)Perl CGIのキャッシュ環境
FastCGI対応は書き間違えだったのですが、どうせならということで対応してみました(対応版の配布はβ4以降になります)。
Perl CGIのキャッシュ動作比較
気づいたらあれこれ対応しすぎた感じがありますが、せっかくなのでどれが一番速いのか試してみしまた(笑) いずれの設定も全くチューニングも何もしていませんので、参考程度にお願いします。
CPU : Pentium3 800MHz (133*6/Coppermine) OS : FreeBSD 6.1-PRERELEASE Web : Apache 2.2 (prefork) ETC : DBキャッシュON TEST : ab -n100 -c4 http://127.0.0.1/xxx/adiary.cgi/user/
テストしたページには適当な日記が数件表示されています。結果は数回実行して頻出値に近い値を取りました。
動作モード | Req/sec | ms/Req |
---|---|---|
cgi | 2.10 | 477.270 |
speedycgi | 15.19 | 65.825 |
mod_speedycgi | 26.41 | 37.863 |
mod_fastcgi | 21.28 | 46.990 |
mod_fcgid | 21.21 | 47.157 |
mod_perl2*1 | 26.69 | 37.473 |
text-file | 196.7 | 5.084 |
mod_fastcgiとmod_fcgidは差がなくて、mod_perl/mod_speedycgiが一歩前に出てるという感じですね。worker動作(スレッドモデル)となると、対応しているのは mod_perl2 vs mod_fcgid だけ。mod_perl2 はいかんせん導入が面倒くさいので、手軽さでは mod_fcgid の方がよいのかもしれません。
本格的にパフォーマンスを求めたり、高負荷時のメモリ消費量の少なさを考えると mod_perl2 on worker MPM に優る選択肢はないのですが個人では必要ないでしょう。*2
ただ、どれも Apache にモジュールを組み込まないとならないので、お手軽に高速化したい場合はSpeedyCGI(ソースコード)をオススメします。パフォーマンスも(個人で使うには)十分ですし、Apacheからは完全にcgiとして見えるので(プロセスが完全に分離するので)、精神的にもよいです。
誰か同じことをMTの管理画面で試さないかなぁ(笑)
各方式の特徴
せっかくなので、簡単に仕組みと特徴を説明。
Perl/cgi
通常のcgi動作。Apacheが、該当のcgiプログラムを fork し実行します。Perl/cgiでは、cgiファイル(使用ライブラリファイル群)のコンパイル時間がとても長く、度々問題となります。*3以下の方式は、どれもこのコンパイル時間を減らすことで大きな高速化を果たしています。
SpeedyCGI(=PersistentPerl)/mod_speedycgi
Apacheからみたら通常のcgiプログラムですが、スクリプト1行目にPerlではなくSpeedyCGIを起動するための
#!/usr/bin/speedy
を記述することでSpeedyCGIが動作します*4。SpeedyCGIは内部的にPerlを呼び出し、実行終了後もそのプロセスを常駐させ(バックエンドと言う)、続いてリクエストが来たときに空いたバックエンドがあればそれに処理を行わせます。
スクリプト側から見た場合、Apacheプロセスと分離されることを除けば*5、全体的な動作はほとんどmod_perlと同じです。
歴史が浅いのか何なのかいまいちマイナーだけど、導入も容易で、mod_perlでは動作しないスクリプトも動作し、Apacheとは別プロセスになるので使うのも気楽です。SpeedyCGIをインストールしてあるレンタルサーバもみかけます。
mod_speedycgiはSpeedyCGIの管理プログラム起動コストを押さえるためApacheにモジュールとして組み込んだものです。それによりかなり高速化されますが、worker MPMには非対応なので注意が必要です。
FastCGI/mod_fcgid
SpeedyCGI的な考え方をより推し進めて、Apache内部にバックエンドプロセスを管理する機能をモジュールとしてインストールしたものです。SpeedyCGIでは、いくらバックエンドがあるとはいえ結局は別プロセスである SpeedyCGI(/usr/bin/speedyそのもの)を fork して実行する必要がありましたが、FastCGIではバックエンドを直接操作するため fork のオーバーヘッドがありません。
mod_fastcgi(FastCGI)とmod_fcgidの差ですが、前者をスレッド動作のApacheに組み込むのはあまりよろしくないとのことです。また巷の噂によると後者の方が速いとかなんとか。なお、FastCGIをスレッド動作のApacheに組み込んだとしても、CGIプログラム(Perlなど)自体は1プロセス=1クライアントとなります。
cgiスクリプトはFastCGI向けに改造する必要があります。
use FCGI; my $count = 0; my $request = FCGI::Request(); while($request->Accept() >= 0) { print("Content-type: text/html\r\n\r\n", ++$count); }
mod_perl
バックエンドどころか、「Perl自体をApacheに抱え込んでしまえばいい」という荒技的な解決策を提示するのが、mod_perl。荒技なだけに、もっともオーバーヘッドが少ない*6方式です。mod_perlの特徴として、Apacheの内部動作をスクリプト側から事細かに制御できますが、移植性(配布性)を考えたソフトではあまり使うことはありません*7。
mod_perlには、Apache 1.x系向けのいわゆるmod_perl(以下mod_perl1と表記)と、Apache 2.0/2.2系向けのmod_perl2があり全く互換性がありません*8。この互換性が度々問題となって、mod_perl1向け書かれたcgiをmod_perl2で動かそうとすると誤動作して厄介な問題を引き起こします。その代表選手とも言えるものがMovable Type*9。
スクリプト側から見た場合、mod_perl1やスレッド動作でないApache2ならば、およそSpeedyCGIと同じような感じです。ただ、Apache2が真価を発揮する worker(スレッド)動作でのmod_perl2は、Perlスクリプト自体もスレッド動作することで高い効率が得られる一方、カレントディレクトリを指定出来ない(chdir()できない)こと*10、あらゆるスクリプトがマルチスレッドでライブラリ空間(メモリ空間)を共有すること*11が大きなネックになります。後からこの対応を行うことは困難であり、これがmod_perl2向けスクリプトがほとんど存在しない原因になっていると思われます。
余談
Apache2(Prefock) + mod_perl2 のときは、次のようにすると自動で chdir されます(いずれか1つ選択)。
PerlResponseHandler ModPerl::RegistryPrefork PerlResponseHandler ModPerl::PerlRunPrefork