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

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 コマンドで計測してみましたので参考までに。

実現方法実行速度
FileHandle100ms
IO::File94ms
Symbol23ms
直接指定14ms

こういう計測の結果、adiary では Symbol モジュールを使っています。

*1 : 逆に言えば、Socket通信のハンドルを作り、出力を自動的にフラッシュしたいときなどは FileHandle や IO::File モジュールを用いて $fh->autoflash(1) などとするのがベターです。

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/06/12(月)定義済の関数を調べる

can メソッドの罠

Perlのマニュアルによれば、オブジェクトのメソッドを調べるには、すべてのクラスで継承される UNIVERSAL クラスの can メソッドを用いて

if ($obj->can("function_name")) { $obj->function_name(); }
if (MYCLASS->can("function_name")) { MYCLASS->function_name(); }

としろと書いてあります。実際この判定はうまく動き、スケルトンシステム(Satsukiシステム)が今ほど本格的になる前のテンプレートに少し毛が生えた程度のころは、この can メソッドを使って関数呼び出しと変数の判別をしていました*1

この can メソッドには罠があって、オートローダー(AUTOLOAD 関数/メソッド)が定義されているときcanメソッドは常に true を返します。AUTOLOAD というのは、もともとはPerlの関数を必要なときに動的ロードする仕組みであって、Satsukiシステムでは独自のオートローダーを定義しています*2。さらに、呼び出された関数が未定義のときに呼び出されるという仕組みを用いて、あたかも関数のような動作を動的生成ということも可能です。AUTOLOADは、未定義関数をロードして代理で呼び出すなり、あたかもその関数が存在するように振る舞うなりすればいいということになります。

# オブジェクト内のハッシュを返すメソッド
# 呼び出した未定義関数はグローバル変数「our $AUTOLOAD」に入っている。
sub AUTOLOAD {
	# $AUTOLOAD='MYCLASS::FUNCTION' → $func='FUNCTION'
	my $func = substr($AUTOLOAD, rindex($AUTOLOAD, '::')+2);
	my $self = shift;
	return $self->{$func};
}

*1 : この時代は、存在する関数名と同じ変数名を使えなかったんですね……懐かしい

*2 : なぜそんな仕組みが必要になるかと言うと、perlの実行で一番遅いのはperlのソースをperlインタプリタが実行可能な形式にコンパイルする処理でして、普段は滅多に使用しないようなデータベース初期化関数やら日記帳を削除する関数やらを常にロードしてコンパイルするのはcgi動作を考えたときに大きなロスとなります。

canメソッドが正常に働くとき、働かないとき

Satsukiシステムのオートローダーは、

Auth.pm
Auth_auto.pm
Auth_auto2.pm

という具合に、目的の関数が見つかるまで次々と連番のモジュールをロードしていくという方式で*3、AUTOLOAD内で次々とモジュールをロードしながら、canメソッドを使用して目的の関数が見つかったか調べる方法を取っていました。そして見つからなかったら失敗(die)するように作ってありました。

実際この方式はうまく動作していまして、このことからAUTOLOAD関数が存在するクラス内のcanはそのAUTOLOAD関数内では正常に判別できることがわかります。たぶんperl自身がcanの中でこの辺をこまめに判別しているようです。

*3 : というのも、関数1つを1つのファイル分割する標準AutoLoaderモジュールでは、いかんせん1つ1つをロードするオーバーヘッドが大きすぎる

AUTOLOADの暴走(無限ループ)

しかし更にややこしい問題が発生しました。SatsukiシステムのDBモジュール*4は、そのAPIが規定されていることが利点となり、どのような下位モジュールに対してもキャッシュを行うDB_cache.pmというものが存在します。この中では、select のみオーバーライドしキャッシュを行い、その他の関数はcanで調べてその関数が存在するときは下位のモジュールの関数を呼び出すという仕組みにしていました。この下位モジュールを呼び出す仕組みでAUTOLOADを使用していたのです。

分かりずらいので図式すると、今このサーバではPostgreSQLおよびこのキャッシュモジュールを使用しています。このときデータベースへのアクセスは、

(キャッシュなり)Diary.pm → DB_pg.pm
(キャッシュあり)Diary.pm → DB_cache.pm → DB_pg.pm

となっています。Diary.pm からは、キャッシュがあろうがなかろうが、DBモジュールAPIを満たすものに対するアクセスしか行っていないため何ら違いはありません。

ここで、プログラム中にたまたま Diary.pm から DB_pg.pm に存在しない関数xyz()を読んでしまったのが問題の始まりでした*5

モジュールAUTOLOAD内
DB_cache.pmDB_pg.pm に xyz() が存在しないのに xyz() を呼び出している
DB_pg.pmxyz() のロードに成功していないのに*6、なぜかxyz()が存在すると思って呼びに行く

結果として「DB_cache.pm → DB_pg.pm → DB_cache.pm → …」という無限ループに陥ってしまいました。DB_cache はその仕組み上、DB_pg.pm を動的に継承してしまうため、DB_pg への関数呼び出しは、例えDB_pg自身からであってもすべて DB_cache を経由します。それでこういうことが起こったわけです。

しかし、どちらのモジュールでも、こういうことを避けるために「canメソッドによって関数が実在するか調べていた」ハズです。調べてみると、どちらのcanも、どんなメソッドに対しても成功していました。ありもしないでたらめな関数でもなんでもif ($obj->can("xxx"))が成立していたわけです。

これが最初に言った、AUTOLOAD関数が定義されているときcanメソッドは常に true を返すという罠です。オブジェクトを動的に継承したために(実際に継承するのではなく、あたかも継承したようにみせかけた*7ために)、自分のクラス外のcanが常に成功したわけです。

*4 : adiaryでも使用

*5 : mod_perl環境で無限ループに陥るとhttpdプロセスと同一であることが災いして、httpdがふくれあがりシステムのリソースを際限なく食いつぶします(汗)

*6 : 存在しないのだから当たり前

*7 : これはいかなるDB下位モジュールに対しても、汎用的にキャッシュモジュールを挟める仕組みを作るためにどうしても必要

解決策

はじめからcanなんて必要なかったんじゃないかというくらい、実に簡単な解決策でした。DB_pg.pm、DB_cache.pmをそれぞれ次のようにしました。

sub AUTOLOAD {
	(略)
	if (defined &$AUTOLOAD) { $can=1; last; }
sub can {
	my ($self, $func) = @_;
	my $func = $self->{_super_class} . '::' . $func;
	return (defined &$func);
}

そしてたぶん、この方法の方が速く動作します。

注意

ここでは便宜上DB_pg.pm内に AUTOLOAD が存在するとして説明しましたが、実際には、Autoloader.pmからAUTOLOAD関数をインポートしています。