Perl OO におけるオーバーヘッド

フレームワークを考えるにあたって、気になる部分のベンチマークを取ってみた。
ポイントは次の3点。

  1. 関数の呼び出し方法: Class::func() と Class->func() 形式
  2. クラスを継承した場合のペナルティ: Class->() と SuperClass->()
  3. 連想配列への直接アクセスと、アクセサ経由のアクセス

Perl における関数型の実装と OO の実装で、関数呼び出し/メソッド呼び出しでどの程度のオーバーヘッドの差があるかをベンチマークした結果。勉強になります。結果としては関数型に対して OO の方が数倍遅い、という結果。

それで、結論の方なのですが

本来なら、アプリケーションより下位にあたるライブラリ関連は、オブジェクト化されて mod_perl 上で共有されるメリットはあるかもしれないが、アプリケーションの上位にあたるフレームワークは、mod_perl 上で共有される意義はあまりない(少なくとも、ライブラリほどではない)と思われる。

というのは少しツッコミを入れておきたいかな、と思います。

まず、Perl で OO な実装をしたときの速度で最も問題になるのはどこか、というところなのですが、メソッド呼び出しよりも遥かに大きいのがクラスのロードにかかってくるオーバーヘッドです。OO で実装すると、アプリケーションの起動時に多くのモジュール = クラスをロードする必要が出てきます。ご存知の通り Perl はアプリケーション実行時にソースコードを一度コンパイルするのですが、モジュールが多数になった場合、そうでない場合に比較してそこに必要とされる処理量はかなりのものになるようです。

これは別に Perl に限った話ではありません。Java にしても他の言語にしても、アプリケーション起動時にはどうしても何かしらのオーバーヘッドがかかります。1リクエストごとにウェブサーバーのサブプロセスを生成〜終了を繰り返す CGI のような環境では、1リクエスト毎にアプリケーションが起動し終了するため、このオーバーヘッドがアプリケーションの処理サイクルの中でかなり支配的なものになります。

そこで、起動時のオーバーヘッドをなくすために、プロセスを終了させずにコンパイル済みのコードをメモリ上にロードしたままにしておき、次回アクセス時にもそれを使いまわすという永続化の手法でこの問題を解決しましょうというアプローチが取られることになります。それが Java Servlet だったり mod_perl だったり FastCGI だったりします。

ということで、OO な Perlウェブアプリケーションフレームワークは、mod_perl の恩恵にあずかれる最たるもの、というのが僕の認識です。

逆に mod_perl 環境下で動作させることができない場合は、OO 実装によるオーバーヘッドを回避することができないためしんどい、ということになります。が、これもある程度の回避方法があります。起動時に極力最低限のクラスのみロードしておき、あとは必要になったところで動的にロードする、という方法です。具体的には、最低限のクラスはスクリプト先頭で use でロードして、それ以外はメソッドの中などで require や eval & use により呼び出す、という方法です。

引用元でも考察が加えられている Movable Type は、配布型パッケージであるため CGI でも動くし mod_perl でも動くという実装になっています。CGI で動作させる場合を考えると、起動時のオーバーヘッドを抑えなければならないので、主な外部ライブラリは require によりなるべく必要になる直前でロードするような実装が施されているようです。Movable Typemod_perl 環境下で動作させると劇的に速度が向上するのは、一度読み込んだモジュールを mod_perl が永続化するため、起動時とrequireによる動的ロードのオーバーヘッドがなくなるためです。

一方、はてなが提供する一連のサービスのようなサーバーサイドアプリケーションでは、この辺を一切考慮しなくていいので、起動時に一気にまとめてロードするのが吉です。mod_perl に関しては、サブプロセスで初めてロードされたモジュールは、親プロセスとの間でメモリ共有が為されないという仕様になっていて、Apache の起動時にすべてを読み込ませる方がメモリの節約になります。そこではてなでは、そのサービスに必要なモジュールは、まとめて全部ロードするように startup.pl に工夫を施してあります。

ということで、mod_perl にすることで Perl OO の最大の問題点は払拭できます。ここまで来てようやくメソッド呼び出しのオーバーヘッドを考える必要がでてくるわけですが、ウェブアプリケーションのライフサイクルの中では、関数呼び出しとメソッド呼び出しの差というのは相対的に考えて、ほとんど無視できると言ってよいかと思います。それよりも、ネットワークでのやりとり、ディスクI/O やデータベースアクセス、fork によるサブプロセスの生成といった外的な要因が支配的になることがほとんどで、それらをどれだけ最小限にまで抑える努力をしたとしても、メソッド呼び出しを使わずに関数呼び出しにする効果が支配的になるほど抑えられることはないと思います。

あとはもう一点、特にこれが重要なポイントだと思います。以前にも引用した、Damian Conway の OO 本の一節です。

一般には、オブジェクト指向Perlによるシステムの実装は、それと等価の非オブジェクト指向実装よりも高速になることはなく、実際には比較して通常20〜50パーセントほど低速になる。
この数字はオブジェクト指向Perlから多くのユーザを遠ざけるほど十分に大きいかもしれないが、オブジェクト指向の設計面および実装面のそれを補うさまざまなり点を見逃すのは悲劇的である。
(中略)残念ながら多くの人は、「20〜50パーセントの低速化」という数字に惑わされ、過去6か月間でプロセッサ速度が2倍になったにもかかわらず、何を意味しているか忘れがちになる。

CPUの進化を考えれば、Perl OO における速度低下によるデメリットよりも OO によって得られるメリットの方が圧倒的に大きいのだ、という話。これは僕も激しく同意なのです。

ということで、フレームワークになんにしても、将来の保守性のことを考えるのであれば Perl OO でがんがん書いていきましょう、という話でした。

オブジェクト指向Perlマスターコース―オブジェクト指向の概念とPerlによる実装方法

オブジェクト指向Perlマスターコース―オブジェクト指向の概念とPerlによる実装方法

追記: C や Java のようなスタティックな言語に比較して Perl が不利になる箇所は純粋な意味での計算処理です。Perl を使っていることでそれが問題になる場合は、それを XS 実装にして解決するという方法があります。はてなプログラマはみな Perl は得意だけれどもそのほかの言語には疎い、という状況だったのですが、Perl よりも低いレイヤでの実装が得意なid:higeponはてなに join してくれたおかげで、その辺の幅が広がってきました。既にいくつか XS で実装し直してもらった処理なんかもあって、その効果はかなり大きなものになっています。

あとは、XML 関連の処理も同じようなものです。リクエストの数に対して XML の parse の処理回数が比例するようなケースで、且つリクエスト数が相当なものに上る箇所では XML parser を利用するオーバーヘッドが大きいので、そういう場合は parser は使わずに正規表現を使うなどして処理速度を向上させたり、ということもやっています。(内部で expat を使う XML::Parser を使っているモジュール、例えば XML::RSS なんかはその最たる例でしたが、これに関しては XML::RSS::LibXML という expat よりもかなり高速な実装が出てきているので、最近はそちらを使って正規表現は使わないようにもなってたりします。)