- 2005-11-29 (火)
- Perl
「XML::Simpleは処理速度が遅い」説といえばNDO::Weblogさんの記事 Perl で XML の処理はどれが速いかベンチ がすぐに思い浮かびます。初見した時はずいぶん感心した覚えがあり、以来、業務で XML::Simple を使う事に多少なりとも躊躇するようになったものです。そんな中、現在仕事で XML parsing with Perl な案件を受け、CPANにUPされている様々な XML 解析系モジュールの処理速度を計測しているのですが、その際気付いた、XML::Simple の意外な落とし穴についてまとめてみました。
まとめ
だらだら説明するのもナンなので、いきなり結論から。XML::Simple は、デフォルトの状態だと
XML::SAX がインストールされている場合、明示的な指定が無い限り、XML::Parser よりも XML::SAX 側のパーサーを優先的に利用する
らしいです。てっきりパーサー指定をまったくしない場合は XML::Parser を呼び出してくれているんだろうと勝手に認識しちゃっていましたが、実はその逆でした。この事は perldoc の ENVIRONMENT セクションにちゃんと詳しく書かれていて、簡単に(テキトウに)まとめると、
- 環境変数「XML_SIMPLE_PREFERRED_PARSER」またはパッケージ変数「$XML::Simple::PREFERRED_PARSER」にて利用したいパーサーを指定できるよ
- これら設定が undef の場合、XML::SAX を優先的に使うよ
- これら設定は初期状態では undef だよ
てな具合です。さらに、これら設定が undef の場合、XML::SAX が内部的に利用するパーサーはユーザが最後にインストールしたSAXパーサーになるそうで。つまり、XML::LibXML や XML::SAX::Expat 等のオプショナルなSAXパーサーがどれもインストールされていない環境では、
PurePerlな、でも決して高速では無い XML::SAX::PurePerl パーサーが使われる
場合もあるって事。詳細は後述しますが、$XML::Simple::PREFERRED_PARSER = 'XML::Parser' および 'XML::SAX::PurePerl' それぞれでこんな感じのXML(75KB)を500回 parse してみたベンチマークコードの実行結果は
$ ./bench1.pl sampleLarge.xml Benchmark: timing 500 iterations of SimpleSAX, SimpleXP... SimpleSAX: 1068 wallclock secs (202.55 usr + 865.64 sys = 1068.19 CPU) SimpleXP: 40 wallclock secs ( 39.47 usr + 0.47 sys = 39.94 CPU)
と、その差は顕著でした。何気なくベンチマークとってみたら極端に処理速度が遅かった・・・そんな時は裏で XML::SAX::PurePerl パーサーが選択されてしまっている可能性を疑ってみてください。
以上のように、単に「XML::Simpleの処理速度」と言っても、利用するパーサーによって速度はまったく異なります。どれを使っとけば無難かと言えば、naoya氏の別記事 XML::Simple におけるパーサーの実行速度比較 にもあるとおり、 XML::Parser が良い感じなので、以後、XML::Simple を利用する場合は漏れなく
$XML::Simple::PREFERRED_PARSER = 'XML::Parser';
とソースコードに記述しておくのが良いかと。処理速度の劇的改善を目の当たりにして、XML::Simple が急に頼りに思えてくるかもしれませんよ。
デフォルトで呼ばれるXML::SAXパーサーの確認方法
XML::SAX の仕様的には
明示的な指定が無い限り、最後に環境にインストールされたSAXパーサーを利用する
との事です。「じゃあ俺の環境で一番最後にインストールされたパーサーは何なの?」という疑問に対しては、以下のコードを実行する事で確認できます:
use XML::SAX;
use Dumpvalue;
my $p = XML::SAX->parsers();
Dumpvalue->new->dumpValue($p);
### 以下実行結果例 ### 0 HASH(0x81dba7c)
'Features' => HASH(0x81b1864)
'http://xml.org/sax/features/namespaces' => 1
'Name' => 'XML::SAX::PurePerl'
1 HASH(0x81b1a8c)
'Features' => HASH(0x81b1840)
'http://xml.org/sax/features/namespaces' => 1
'Name' => 'XML::LibXML::SAX::Parser'
2 HASH(0x8166fa0)
'Features' => HASH(0x8166fd0)
'http://xml.org/sax/features/namespaces' => 1
'Name' => 'XML::LibXML::SAX'
XML::SAX のソースコードを読むと、一番最後にインストールされたパーサー = この配列の最後尾のパーサーになっていますので、この例の場合は XML::LibXML::SAX パーサーが使われる、という事になります。
※参照元ソースコード:
XML::SAX::ParserFactory の _parser_class 関数辺り
XML::SAX の load_parsers 関数辺り
ベンチマーク比較
ここでは XML::Simple のパーサーを変えた場合の速度比較に加えて、XML::Simple は使わずに、同様のPerl構造体変換を XML::Parser モジュールにて実施した場合との速度比較もしてみました。なので計測対象は以下の3つになっています;
- $XML::Simple::PREFERRED_PARSER = 'XML::Parser' と明示した状態の XML::Simple
→ XML::Simple高速モード - $XML::Simple::PREFERRED_PARSER = 'XML::SAX::PurePerl' と明示した状態の XML::Simple
→ XML::Simple低速モード - XML::Parser モジュール単体で同様のPerl構造体変換を行ったもの
→ XML::Parser
※XML::Simple低速モードは利用パーサーを明示的に指定していない状態($XML::Simple::PREFERRED_PARSER = undef)で、かつ XML::LibXML::SAX 等の追加SAXパーサーをインストールしていなかった場合(最悪・最遅の場合)を想定してのモード。
ベンチマーク用のソースコードはこんな感じ:
#!/usr/bin/perl
use strict;
use warnings;
use lib './lib';
use Benchmark;
use Dumpvalue;
use encoding 'utf8';
use XML::Simple;
use XML::Parser;
die "Usage: ./bench.pl xmlfile\n" if(!@ARGV);
my $xml; { open(IN,$ARGV[0]) or die;
local $/ = undef;
$xml = <IN>;
close(IN); }
sub simpleFast {
$XML::Simple::PREFERRED_PARSER = 'XML::Parser';
my $p = XML::Simple->new();
my $r = $p->XMLin($xml,ForceArray=>1,KeyAttr=>{}) or die;
}
sub simpleSlow {
$XML::Simple::PREFERRED_PARSER = 'XML::SAX::PurePerl';
my $p = XML::Simple->new();
my $r = $p->XMLin($xml,ForceArray=>1,KeyAttr=>{}) or die;
}
sub xmlParser {
my $p = new XML::Parser(Style=>'Simple');
my $r = $p->parse($xml);
}
timethese(500,{
SimpleFast => 'simpleFast($ARGV[0])',
SimpleSlow => 'simpleSlow($ARGV[0])',
XmlParser => 'xmlParser($ARGV[0])',
});
テスト用XMLデータやその他必要ソース一式を含めたアーカイブもここに置いておきます。
で、まずは小さめなサイズ(2KB)の、日本語が含まれた UTF-8 なXMLデータで速度計測してみたところ、結果は以下のとおりでした:
$ ./bench.pl sampleSmall.xml
Benchmark: timing 500 iterations of SimpleSlow, SimpleFast, XmlParser...
SimpleSlow: 32 wallclock secs ( 1.66 usr + 30.45 sys = 32.11 CPU)
SimpleFast: 2 wallclock secs ( 0.82 usr + 0.76 sys = 1.58 CPU)
XmlParser: 1 wallclock secs ( 0.93 usr + 0.16 sys = 1.09 CPU)
すでに XML::Simple 低速モード (=XML::SAX::PurePerlパーサー) の遅さが顕著に現れていますね。では続いて、もうちょっと大きめなサイズ(75KB)のXMLデータで計測してみると:
$ ./bench.pl sampleLarge.xml
Benchmark: timing 500 iterations of SimpleSlow, SimpleFast, XmlParser...
SimpleSlow: 1068 wallclock secs (202.55 usr + 865.64 sys = 1068.19 CPU)
SimpleFast: 40 wallclock secs ( 39.47 usr + 0.47 sys = 39.94 CPU)
XmlParser: 20 wallclock secs ( 0.36 usr + 20.39 sys = 20.75 CPU)
てな感じになりました。
以上、ここまでの結果から、
XML::SAX::PurePerl 利用の XML::Simple だと速度は激遅になるけれど、パーサー指定を 'XML::Parser' と明示した際の XML::Simple ならばそれなりに高速動作する
事が推測出来るのではないかと思われます。
関連情報
おまけ編 - 背後で利用している XML::SAXパーサーの確実な確認方法
おまけ編 - $/ 変数の状態によって利用パーサーが変わる妙について
Comments:2
- ゆうすけ 2006-12-05 (火) 09:23
-
これは知らなかった!
- bashi 2006-12-11 (月) 15:12
-
perldocの一番冒頭に書いておいて欲しいですよね、この隠れ仕様。