- 2006-07-04 (火)
- Perl
GTop - Perl interface to libgtop
perl.apache.org にある mod_perl パフォーマンスチューニングのドキュメントにて解説されていた、ある特定のモジュールを use した際のメモリ増加量を調べる方法。GTop っていうCPANモジュールを、こんな感じに使うことでお手軽に調べることができるようです:
gtop.plx
#!/usr/bin/perl
use strict;
use warnings;
use GTop;
my $gtop = GTop->new;
my $before = $gtop->proc_mem($$)->size;
eval $ARGV[0];
die $@ if $@;
my $after = $gtop->proc_mem($$)->size;
my $diff = GTop::size_string($after - $before);
print "$diff : $ARGV[0]\n";
例) CGIモジュールのメモリ増加量は...
$ ./gtop.plx 'use CGI'
704k : use CGI
あら面白い♪って事で、思いつくCPANモジュールを片っ端から調べてみました。
目次
- 結果 - CGI系
- 結果 - Template系
- 結果 - Encode, encoding
- 結果 - DBI系
- 結果 - 日時操作系
- 結果 - Class::Accessor
- 結果 - LWP系
- 結果 - XML系
- 結果 - File操作系
- 結果 - Net系
- 結果 - Apache系
- 結果 - Socket通信系
- 結果 - HTML::Parser
- 結果 - JSON系
- 結果 - *::Util系
- 結果 - その他
- メモリを有効利用するために
- 調査結果から見えてくる疑問点
- おまけ - GTop モジュールのインストールは面倒かも
わんさか調べてみた結果
※調査用に用意したスクリプトはこんな感じです。
観点
mod_perl パフォーマンスチューニングで重要な「shared memory を有効に使う」の観点から、普通に use する場合に加えて、startup.pl 内での呼び出し記述 - import 無しの 'use CGI ()' - も一緒に調べてみました。さらには、気になったので、実際にインスタンスを作ってみた場合とか、メインのメソッドを利用してみた場合のメモリ増加量も調べてみた次第。
CGI系
704k : use CGI
704k : use CGI ()
788k : use CGI (); CGI->compile(qw(param header))
788k : use CGI (); CGI->compile(qw(param header)); CGI->new
2.1M : use CGI (); CGI->compile(":all")
2.1M : use CGI qw(-compile :all)
264k : use CGI::Carp
264k : use CGI::Carp ()
264k : use CGI::Cookie
264k : use CGI::Cookie ()
264k : use CGI::Cookie (); CGI::Cookie->new(-name=>"hoge")
264k : use CGI::Lite
132k : use CGI::Lite ()
264k : use base "CGI::Application"
264k : use base "CGI::Application"; __PACKAGE__->new
1.0M : use base "CGI::Application"; __PACKAGE__->new->run
CGI モジュールは例によって shared memory 用の呼び出し方とか色々あるので、主要と思える記述を試してみました。
Template系
940k : use Template
940k : use Template ()
1.6M : use Template (); Template->new
3.1M : use Template (); Template->new->process("x.tmpl",{})
2.9M : use Template (); Template::Config->preload
808k : use HTML::Template
808k : use HTML::Template ()
808k : use HTML::Template (); HTML::Template->new(filename=>"x.tmpl")->output
TTの方はデフォルトのTemplate::Stash:XSを使った場合の結果。 別途、空ファイル tt.tmpl を用意しておく必要あり。
Encode, encoding
428k : use Encode
432k : use Encode ()
584k : use Encode::Guess 1.9M : use Encode::Guess qw(euc-jp)
1.9M : use Encode::Guess qw(euc-jp shiftjis iso-2022-jp)
460k : use encoding "utf8"
476k : use encoding "utf8", Filter=>1
DBI系
1.0M : use DBI
1.0M : use DBI ()
1.7M : use DBI (); DBI->connect($dsn,$id,$pass)
1.7M : use DBI (); DBI->install_driver("mysql")
2.2M : use base "Class::DBI"
2.2M : use base "Class::DBI"; __PACKAGE__->connection($dsn,$id,$pass)
DBD::mysql を使った場合の結果。
日時操作系
1.9M : use DateTime
1.9M : use DateTime ()
2.1M : use DateTime (); DateTime->now( time_zone=>"local" ) 132k : use HTTP::Date
132k : use HTTP::Date ()
132k : use HTTP::Date (); HTTP::Date::time2str()
Class::Accessor
132k : use base "Class::Accessor"
132k : use base "Class::Accessor"; __PACKAGE__->mk_accessors("hoge")
132k : use base "Class::Accessor::Fast"
132k : use base "Class::Accessor::Fast"; __PACKAGE__->mk_accessors("hoge")
LWP系
1.0M : use LWP::UserAgent
1.0M : use LWP::UserAgent ()
1.0M : use LWP::UserAgent (); LWP::UserAgent->new
2.8M : use LWP::UserAgent (); LWP::UserAgent->new->get("http://example.com")
264k : use LWP::Simple
132k : use LWP::Simple ()
1.3M : use LWP::Simple qw($ua)
1.2M : use LWP::Simple (); LWP::Simple::get("http://example.com")
XML系
628k : use XML::Parser
628k : use XML::Parser ()
628k : use XML::Parser (); XML::Parser->new
912k : use XML::Parser (); XML::Parser->new(Style=>"Objects")->parse("<x>fuga</x>")
3.5M : use XML::DOM
3.5M : use XML::DOM ()
3.6M : use XML::DOM (); XML::DOM::Parser->new
2.3M : use XML::LibXML
2.3M : use XML::LibXML ()
2.3M : use XML::LibXML (); XML::LibXML->new->parse_string("<x>fuga</x>")
524k : use XML::Simple
396k : use XML::Simple ()
396k : use XML::Simple (); XML::Simple->new
4.3M : use XML::Simple (); XML::Simple->new->XMLin("<x>fuga</x>")
1.4M : use XML::Simple (); $XML::Simple::PREFERRED_PARSER="XML::Parser"; XML::Simple->new->XMLin("<x>fuga</x>")
264k : use XML::TreePP
264k : use XML::TreePP ()
264k : use XML::TreePP (); XML::TreePP->new
264k : use XML::TreePP (); XML::TreePP->new->parse("<x>hoge</x>")
File操作系
544k : use File::Find
544k : use File::Find ()
132k : use File::Basename
132k : use File::Basename ()
280k : use File::Path
280k : use File::Path ()
132k : use File::Spec
132k : use File::Spec ()
156k : use File::Glob
156k : use File::Glob ()
564k : use FileHandle
564k : use FileHandle ()
564k : use FileHandle (); FileHandle->new
432k : use IO::File
432k : use IO::File ()
432k : use IO::File (); IO::File->new
432k : use IO::File (); IO::File->new->open(">hoge")
Net系
1.8M : use Net::FTP
1.8M : use Net::FTP ()
1.5M : use Net::SMTP
1.5M : use Net::SMTP ()
Apache系
636k : use Apache::Session::File
764k : use Apache::Session::File; tie my %x,"Apache::Session::File",undef
132k : use base "Apache::Singleton"
132k : use Apache ()
396k : use Apache::Registry ()
264k : use Apache::PerlRun ()
Socket通信系
972k : use IO::Socket
132k : use IPC::Open2
132k : use IPC::Open3 292k : use Socket
HTML::Parser
308k : use HTML::Parser
308k : use HTML::Parser ()
308k : use HTML::Parser (); HTML::Parser->new
308k : use HTML::Parser (); HTML::Parser->new->parse("<a>fuga</a>")
JSON
396k : use JSON
396k : use JSON ()
396k : use JSON (); JSON::objToJson({ x=>1 }) 2.3M : use JSON::Syck
2.3M : use JSON::Syck ()
2.3M : use JSON::Syck (); JSON::Syck::Dump({ x=>1 })
*::Util系
132k : use Hash::Util
0k : use List::Util
0k : use Scalar::Util
※0K = GTopにて使用されているモジュールゆえ判定不能
その他
132k : use Attribute::Handlers
132k : use Attribute::Handlers ()
152k : use Digest::MD5
152k : use Digest::MD5 ()
264k : use Dumpvalue
132k : use English
132k : use English ()
148k : use Fcntl
148k : use Fcntl ()
280k : use Fcntl qw(:DEFAULT)
412k : use FindBin
132k : use Getopt::Std
528k : use Getopt::Long
660k : use Math::Trig
660k : use Math::Trig ()
776k : use POSIX
248k : use POSIX ()
352k : use Storable
352k : use Storable ()
132k : use Unicode::RecursiveDowngrade
132k : use Unicode::RecursiveDowngrade; Unicode::RecursiveDowngrade->new->downgrade({})
メモリを有効利用するために
mod_perl の場合
mod_perl では出来るだけ多くのメモリを共有させることで、より効率的なサーバ資源活用が実現できます (※詳しい説明は長くなるので省略。perl.apache.org にある mod_perl ドキュメント > performance tuning > sharing memory を参照のこと)。なので、各種モジュール (特にメモリ使用量の多いモジュール) を mod_perl 下で使用する場合は、なるべく apache 親プロセス起動時に use しておくとGOODです。具体的には httpd.conf と startup.pl を以下のように設定する感じになります:
httpd.conf
PerlRequire startup.pl
startup.pl
use Encode (); use DateTime (); ... 1;
※余談 - startup.pl って、どう見ても startup.pm なんじゃないかと思うのは俺だけ?%::INCにも追加されるし。
関連情報
Typemiss.net 「WebプログラマのためのCopy On Write解説:mod_perl/FastCGIでメモリを節約する方法」 - メモリ共有について、図解つきで丁寧に説明してくれています。素敵。
cgi の場合
cgi の場合、メモリ共有とかは関係無くなってしまいます。とすると、あんまり語るべきことは思いつかないなあ。メモリ使用量が多いモジュールは使うのを避けよう・・・って話では無いですし。あたりまえか。
先駆者が作ってくれたこれらモジュールは、車輪の再開発をさける意味でも、積極的に利用していくべし、だと思います (たまに駄作もあるかも、だけど、その辺は己のセンスで嗅ぎ取るべし)。で、メモリ使用量が増えた事で起動時間が遅くなって困るとかいう問題が出てくるのであれば、それこそ mod_perl に移行させることをお勧めします。
調査結果から見えてくる疑問点
startup.pl で use しておく、といっても、前述の調査結果をよく見てみると、
例えば LWP::UserAgent の場合
1.0M : use LWP::UserAgent ()
2.8M : use LWP::UserAgent (); LWP::UserAgent->new->get("http://example.com")
のように、単純に use した場合と、実際に利用してみた (getメソッドをコールした) 場合とで、メモリ増加量が大幅に異なっています。真のメモリ共有化・効率化を目指すのであれば startup.pl の中で 2.8M 分きっちりメモリにロードしておいた方が良いのでは?
なぜこんなに差が出ているの?
実際のソースを見るとすぐわかるのですが、例えば前述の LWP::UserAgent の get メソッド付近のコードはこんな感じになっています:
sub get { ... require HTTP::Request::Common; .... }
require 宣言 = get メソッドが実行されないと、HTTP::Request::Common モジュールがメモリ上にロードされない仕組みになっています(require についての解説はエントリ「Perlのuseとrequireの違い」 を参照) 。似たような require 行が HTTP::Request::Common の中にもあって・・・みたいな連鎖が、メモリ使用量の差を生み出しているようです。
こういった require を使ったモジュール動的ロードの仕組みは、通常の cgi で使う分には「必要な時に必要なモジュールだけをロードする」といった観点から、無駄なメモリ消費を抑えられてGOODなのですが、 mod_perl + メモリ共有を考えた場合だと、逆に作用しちゃってますね。
解決方法
最初は別エントリでぐちゃぐちゃ書いてたんですが、ふと気になって Google 検索してみたら先駆者がすでに語ってくれていました (検索してみてよかった。ホッ)。naoyaのはてなダイアリー「startup.pl で require?」エントリ経由 → Practical mod_perl > Chapter 10. Improving Performance with Shared Memory and Proper Forking
10.1.5. Module Initialization at Server Startup
It's important to preload modules and scripts at server startup. But for some modules this isn't enough, and you have to prerun their initialization code to get more memory pages shared. Usually you will find information about specific modules in their respective manpages.
要約すると 「それぞれのモジュールのドキュメント読んでね。mod_perl 専用の初期化方法について触れてある。はず。」みたいな。mod_perl 専門書にてそう書かれているんだから、きっと万能な対策 (silver bullet)は無いのかなぁ。※mod_perl2だとどうなんだろう?未調査。
というわけで、地道に各モジュールごとの対策を調べていくべし。このエントリ前半の調査結果一覧が、対策が必要なモジュールの特定に役立てば幸いです。
TIPS
しょぼいですけど、各モジュールごとの対策を効率よく調べる手順はこんな感じになるかと思います:
- perldoc で文字列 'mod_perl' を検索してみる
例) CGIモジュールの perldoc だと、すぐに CGI->compile を使うべし、みたいな記述が見つかります。 - モジュールのソースコードから、文字列 'MOD_PERL' を検索してみる
例) Template モジュールだと 'Template::Config->preload() if $ENV{ MOD_PERL }' 記述が見つかり、なにもせずとも勝手に mod_perl 最適化されることがわかります。 - それでも見つからないなら、startup.pl の中でメソッドを実行しておいちゃう
→諦めて get メソッドなりなんなりを実行しといちゃえ /w
→多少は無駄があるだろうけど、LWP::UserAgentなんかだと有効な気がします /w
おまけ - GTop モジュールのインストールは面倒かも
GTop モジュールは、 libgtopなるGNOME系のライブラリの Perlインターフェースです。なので事前にこのlibgtopを導入しておく必要があります。未知の世界だったので結構ハマりました。glib-config が無いよ、gnome-config も無いよ、 pkg-config も必要だよ・・・って、もー面倒くさかったです。僕の環境 (Debian Sarge) では、apt-install にて libgtop2-2 の他に、libgnome-dev と pkg-config パッケージを導入する事で無事動くようになりました。ご注意あれ。