Hatena::ブログ(Diary)

naoyaのはてなダイアリー

July 08, 2005

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

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

ポイントは次の3点。

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

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

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

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

Daio Today: 関数呼び出しとクラス継承のベンチマーク

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

まず、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 Type を mod_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 でがんがん書いていきましょう、という話でした。

追記: 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 よりもかなり高速な実装が出てきているので、最近はそちらを使って正規表現は使わないようにもなってたりします。)

koziykoziy 2005/07/09 20:21 はじめまして、いつもこっそり拝見させていただいております(^^;
mod_perlが使えない環境であれば、確かにオーバーヘッドを考慮しないといけないとは思いますが、id:naoya さんの仰る通りI/O関係などによるオーバーヘッドの方が大きいですよね。
Perl の動的・柔軟性による OO を活用する方が Perl を使うメリットとして有効なんじゃないかしら、と個人的には思っておりますです。

RantmanRantman 2005/07/17 03:26 私も「はじめまして」です。

いきなりですが、
OO するんなら、Python や Rubyを使った方が良いでしょう。
Python の場合、モジュールは、一度使うとコンパイルして
保存されるので、次回からは、コンパイルのオーバーヘッドは
無くなります。
また、PsycoというJITみたいのがあって、場合によっては、
Perlを上回る実行速度を出します。

Perlは、オブジェクトの生成も遅かった筈。
次バージョンでどうなるのか知らんですが、
PerlでOO、と聞くと、とろい、醜い、なぜ?と連想をしてしまう。

OOをやらなければ、Perlは、スクリプト言語の中では
一番早いし、手っ取り早く何か片付けるのには良いですが。

Rubyは、Perl的な、汚いコードが好きなら、Pythonよりも
OOがやりやすい面もあるし、最近PHPの馬鹿さ加減に気付いた
人達が移住し始めているので、更に人気が上昇するでしょう。
ただし、PHPで育った人は、セキュリティーとか、
小さい軽いコードを作るという意識が欠けている場合が多いので、
Rubyでもやたらと重い、信頼性の低いソフトが増えるかも。

それ以外の選択肢として、Ocamlとか、Forthも良いかも。
Ocamlは、ネイティブコードにコンパイル出来るし、
OOも、一応やりたいなら出来る。
ただし、文法が古文やってるみたいで、慣れるまで辛い。
このところ、東大とかで、一生懸命教えているので、
今後もう少し普及する可能性あり。

Forthは、x86なら、アセンブラーで書かれた非常に高速の
しかもフリーのものが最近出てきている(下手するとCより早い)。
また、イメージをセーブ出来たりするので、
従来のForthの弱点だった、「起動時間のとろさ」も
無くなるのではないかと思う。
更に、OOを入れたForthというのもあったりするけど、
Forthの場合、OOは、やらんでも良いような気がします。
「スタックに乗ったもの=オブジェクト」みたいなものだし。


後、CPUの性能が向上してるから重くてもいいんだ、
という考え方も確かにありますが、
通常時にはそれで間に合っても、シビアな状態になって、
本当に少しのリソースでも欲しかったり、こけやすい状態に
なったところであだになるので、小さい軽いものを
常に心がけるのがよろしいかと思いました。

Connection: close