XMLアプリケーションのパフォーマンスを改善する 第2回

Xerces2のSAXやDOM実装でパーサのインスタンスを再利用する

3回の記事から成るこのシリーズでは、XMLアプリケーションを書く上でのベスト・プラクティスを解説していますが、今回は著者のElena LitaniとMichael Glavassevichが、Xerces2実装を使うことでSAXやDOMアプリケーションのパフォーマンスをどのように改善するかを説明します。また、パーサのインスタンスを再利用することで、アプリケーションのパフォーマンスを改善する方法を示すコードの例も紹介します。

Elena Litani (elitani@ca.ibm.com), Staff Software Developer, IBM

Elena Litaniは、IBMで働くソフトウェア開発者です。Eclipse.org において、SDO(Service Data Objects)に関するリファレンス実装を提供している、EMF(Eclipse Modeling Framework)プロジェクトの主要な貢献者の一人です。以前は、Apache Xerces2プロジェクトの主要な貢献者として、Xerces2 XML Schema や DOM Level 3 の実装、またパーサのパフォーマンスの解析や改善に取組んできました。W3C DOM Working GroupではIBM代表を務めたこともあり、DOM Level 3仕様の開発にも参加していました。



Michael Glavassevich (mrglavas@ca.ibm.com), Software Developer, IBM

Michael Glavassevichは、IBM Toronto Labのソフトウェア開発者です。2003年にXerces2プロジェクトに貢献を始め、現在は主導的な開発者の一人です。連絡先はmrglavas@ca.ibm.com です。



2004年 7月 30日

今日、多くのアプリケーションでは、SAXまたはDOMパーサを取得するために JAXP (Java API for XML Processing)を使います。JAXPのバージョンによって、また J2SE (Java 2 Platform, Standard Edition)のベンダによって、アプリケーションは異なったパーサ実装を取得します。例えば、Sun J2SE 1.4.x にはCrimsonパーサが含まれており、IBM® J2SE 1.4.x と Sun J2SE 1.5 にはXerces2パーサが含まれています。Xerces2パーサで直接作業したいのであれば、JAXPファクトリ・プラグイン機構を使うことで、JAXPがどこでXerces2パーサ実装を見つけることができるかを指定することができます。

たとえアプリケーションがパーサ実装を指定できないとしても、ある環境においては、利用可能なパーサ実装としてXerces2パーサをまだ期待しているかも知れません。そしてそのパーサに対して、パフォーマンスに影響するような機能やプロパティを設定したいと思うかも知れません。もし、Xerces2パーサが使用できない場合には、Xerces2特有の機能やプロパティを設定しようとしても失敗しますが、パーサの初期化は(以下で説明するように)一度行われるだけなので、アプリケーションのパフォーマンスを妨げることはないはずです。

この記事では、Xerces2パーサを使用している人のために、SAXやDOMアプリケーションのパフォーマンスを改善するためのヒントを提供します。Xerces2では、ほとんどすべてのリリースで何らかのパフォーマンスに対する改善がなされているので、常に最新版を使用するようにしてください。この記事ではまた、パーサのインスタンスを再利用することによって、いかにアプリケーションのパフォーマンスが改善するかについても説明します。ただし、Xercesでは特段にうまく機能するものが、他のパーサではあまりうまく行かないことがあり、その逆もあると言うことを念頭に置いておいてください。

SAX: Xerces2に特有のヒント

前回の記事では、SAXアプリケーションを書く場合のパフォーマンスの改善に関して、一般的なヒントを説明しました。皆さんがXerces2 SAXパーサを使っているのであれば、他にも幾つかのことを考慮する必要があります。この項では、Xerces2のSAXパーサを使うことでアプリケーションのパフォーマンスに影響を与える部分について説明します。

名前空間宣言

Xercesは内部的に、開始タグ要素で指定される他の属性と共に名前空間宣言を保存します。SAXパーサはデフォルトでは、startElementコールバックで報告される属性の中に名前空間宣言を含めません。SAX APIに準拠するためにパーサは、たとえ開始タグに何も名前空間宣言が指定されていない場合であっても、一連の属性に対して、名前空間宣言をすべて削除する動作を繰り返す必要があります。

機能URI http://xml.org/sax/features/namespace-prefixesで規定される機能が、名前空間宣言を報告するかどうかを制御します。よりパフォーマンスを向上させるためには、この機能を真(true)とし、パーサが他の残りの属性の中から名前空間宣言を報告するようにします。ただし Namespaces in XML Recommendation(参考文献参照)の正誤表に規定されているように、パーサは名前空間宣言を、名前空間名http://www.w3.org/2000/xmlns/ を指すものとしては報告しません。この仕様書の初版では、名前空間に名前空間宣言を割り当てていません。SAX 2.0.2のリリース以前には、名前空間宣言は常に、名前空間を持たないものとして報告される必要がありました。Xercesは、内部的にこうした属性を名前空間にバインドするので、パーサはアプリケーションに対して名前空間宣言を属性として報告する前に、名前空間宣言を名前空間からアンバインドする必要があります。同様に、パーサは、どんな名前空間宣言に関しても、存在の確認を一連の属性に対して必ず繰り返し行う必要があります。

SAX 2.0.2では、機能URI http://xml.org/sax/features/xmlns-uris で規定される新しい機能を導入しています。これによって、名前空間宣言が名前空間を持つものとして報告するようにしたいかどうかを明示できるようになります。Xerces 2.7.0 (この記事の執筆時点では、まだ開発中)でこの機能を真(true)として設定すると、それらが既に期待される形式になっている場合、それ以上一連の属性を処理しないようになります。パーサをこのように設定することで、属性の処理速度を上げることができます。

インデックスで属性を読む

Xerces2のorg.xml.sax.Attributes 実装では、インデックスによって属性に高速アクセスできるように、属性は配列に格納されます。SAXヘルパー・クラスorg.xml.sax.helpers.AttributeListImpl(Crimsonパーサで使用され、拡張された)も同様な方法で属性を保存します。属性をこのように保存しておくと、名前ではなくインデックスで属性にアクセスでき、属性処理のパフォーマンスを向上させることができます。この場合、名前で属性を参照すると、線形探索(リニア・サーチ)が開始されます。要素に対して指定される属性の数が多くなると、平均検索時間も増加します。ある属性を名前で検索し、その属性のプロパティのいくつかを検査する場合には、まず名前でそのインデックスを参照し、次にそのインデックスを使って、プロパティの値や型といった対象のプロパティを取得した方が良いと言えます。

属性を配列に保存するのは典型的な実装ですが、要素の属性は順序が揃っているわけではないので、他のパーサ実装では属性をもっと効率的に保存するかも知れません。これはつまり、属性は必ずしも文書の中に現れる順序で保存されるわけではない、ということを意味します。このような場合には、名前で属性を検索した方が、インデックスで参照するよりも高いパフォーマンスが得られる可能性があります。


DOM: Xerces2に特有のヒント

前回の記事では、DOMアプリケーションを書く際にパフォーマンスを向上させるための一般的なヒントを説明しました。この項では、Xerces2 DOMの設計とアプリケーションのパフォーマンスに影響する機能について説明します。

DOM実装を指定する

DOM APIは、幾つかの仕様から構成されています。これらの仕様は、ある特定の仕様が担当する範囲を明確にして機能を定義します。デフォルトのXerces DOM実装は、DOMコア、XML変異イベント、範囲、トラバーサル機能をサポートします。もしアプリケーションでDOM Level 2(または3)のコア勧告のみの実装が必要なのであれば、http://apache.org/xml/properties/dom/document-class-name プロパティを使ってorg.apache.xerces.dom.CoreDocumentImpl クラスをorg.w3c.dom.Document インターフェースの実装として指定することによって、パフォーマンスを改善することができます。この実装はDOM Level 2とLevel 3のコアおよび、Level 3のロードとセーブに関する勧告をサポートしています。この実装では、他のDOM仕様をなにも実装しないため、パフォーマンスが良くなります。

遅延DOM

パーサはデフォルトで、遅延DOM(Deferred DOM)として知られる簡潔な配列構造を使ってDOMを構築します。これによってパーサは、構文解析中にツリーが完全に展開される場合よりも早く文書を返せるようになり、メモリの使用効率も向上します。Xerces2 DOM実装は、配列構造に格納された情報を使用して、ツリーをトラバースしながらDOMノードを作ります。

一般的に、アプリケーションが大きな文書を処理する必要がある場合や、アプリケーションがツリー全体をトラバースするつもりがない場合には、遅延実装を使用すべきです。ただし、一部のパフォーマンス・テストによると、小さな文書(0Kから10K)に対して、遅延ノード展開を使ったXerces2 DOMではパフォーマンスが低下し、メモリ消費が増えることが分かっています。

ですから、小さな文書に対してXerces2 DOMを使う場合には、機能URIhttp://apache.org/xml/features/dom/defer-node-expansionで指定される遅延ノード展開機能を使用不可にすべきです。大きな文書(100K以上)に対しては、遅延DOMは非遅延DOMよりもパフォーマンスが良くなりますが、メモリ消費量は多くなります。

Xerces2 DOMをトラバースする

属性ノードの値を文字列として取得するには、必ずgetValueorg.w3c.dom.Attrインターフェース)メソッドを使うようにします。属性ノードの子供を取得するためにメソッドを使うのを避けるようにします。DOM実装は、属性文字列値に対してTextノードを生成する必要がありますが、Xerces2 DOM実装ではアプリケーションが属性ノードの子供を取得しようとするまで、Textノードを生成するのを遅らせます。アプリケーションがgetValueメソッドを使って属性値を取得する場合には、Textノードが生成されることがないので、メモリ空間を節約することになり、DOMトラバーサルのパフォーマンスも改善されます。

多くのアプリケーションでは、ツリーのトラバースにgetChildNodes()NodeList を使うことを選択しますが、リスト1 に示すように、getFirstChildgetLastChildgetNextSiblinggetPreviousSibling などのメソッドを使った方が、ツリーのトラバースにかかるコストは低くなります。

リスト1. Xerces2 DOMをトラバースする
Element root = document.getDocumentElement();
// Avoid traversing using NodeList:
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
   Node n = children.item(i);
}
// Instead, use the following methods:
Node child = root.getFirstChild();
while (child != null) {
   child = child.getNextSibling();
}

DOMをシリアル化する

Javaオブジェクトのシリアル化を利用して、Xerces2 DOMをシリアル化することは可能ですが、DOMはでき得る限りオブジェクトのシリアル化を使わず、XMLとしてシリアル化するように推奨します。Xerces2 DOM実装では、異なったXercesのバージョン間でDOMのJavaオブジェクトでのシリアル化に互換性があることを保証していません。さらに、非常に大まかな測定によると、XMLでのシリアル化は、Javaオブジェクトでのシリアル化よりもパフォーマンスが高く、XMLのインスタンス文書もオブジェクトをシリアル化したDOMよりも、必要な記録空間が少ないことが分かっています。


パーサを再利用する

パーサのインスタンスを生成しても大してパフォーマンスのコストはかからない・・・、というのがXMLアプリケーションを書く上で一般的に誤解されている点です。それどころか、実際はむしろ逆で、パーサのインスタンスを生成するということは、多くのオブジェクトを必要とし、各々のXML文書を構文解析する度に再利用されるオブジェクトを生成、初期化、設定するということを意味します。こうした初期化や設定の操作は、高いものにつきます。

さらに、JAXP APIを使用していると、パーサの生成はさらに高価なものになります。このAPIでパーサを取得する場合、まず(例えばSAXParserFactory のような)対応するパーサ・ファクトリを取得する必要があり、それを使ってパーサを生成する必要があります。JAXPがパーサ・ファクトリを取得するために使用する検索機構では、最初にClassLoader を参照し(環境によっては、これは高価な操作になります)、次にJAXPシステム・プロパティ(jaxp.propertyファイル)で規定されるパーサ・ファクトリ実装、またはJar Service Provider機構を使用することで規定されるパーサ・ファクトリ実装を見つけようとします。Jar Service Provider機構を使った参照は、クラスパスにある全てのJARを検索する可能性があるので特に高価なものになり、検索に使われたClassLoader がネットワークをも検索する場合には、さらにパフォーマンスが低下します。

従ってパフォーマンスを向上させるためには、アプリケーションでパーサを生成するのは一度とし、その後はこのパーサのインスタンスを再利用するように強く推奨します。

一般的に、アプリケーションには2つの形式があります。

  • 機能やプロパティの設定が同じパーサを必要とするアプリケーション
  • 構文解析毎にパーサ機能やプロパティを変更する必要のあるアプリケーション

最初の形式のアプリケーションであれば、パーサの再利用は容易です。通常は最初にパーサを生成して、機能やプロパティを設定し、その後で同じパーサのインスタンスを使って、全てのXML文書を構文解析します。この例をリスト2 に示します。

リスト2. パーサのインスタンスを再利用する
// Use JAXP to retrieve SAX factory
SAXParserFactory factory = SAXParserFactory.newInstance();
// create a parser instance
SAXParser parser = factory.newSAXParser();
// set features and properties
parser.getXMLReader().setFeature(
    "http://xml.org/sax/features/validation", true);
parser.getXMLReader().setProperty(
    "http://xml.org/sax/properties/lexical-handler", myHandler);
parser.getXMLReader().setErrorHandler(myErrorHandler);
DefaultHandler myHandler = new DefaultHandler();
// use the same parser instance to parse XML documents
for (int i=0; i < args.length; i++){
   parser.parse(args[i], myHandler);
}

2番目の形式のアプリケーションに対して、1つのパーサ・インスタンスだけを使ってパーサのキャッシュ動作を実装するのは、最初の形式のアプリケーションよりも困難です。このような実装は可能ですが、忘れずに機能やプロパティをデフォルト状態にリセットする必要があります。この例をリスト3 に示します。

リスト3. 機能やプロパティ値をリセットする
// record the features used by application and their default values
HashMap defaultFeatureValues = new HashMap();
defaultFeatureValues.put("http://xml.org/sax/features/validation",
          Boolean.FALSE);
...
// record features that are set for this scenario
Vector currentFeatures = new Vector();
currentFeatures.add("http://xml.org/sax/features/validation");
// set features on the parser
parser.getXMLReader().setFeature(
        "http://xml.org/sax/features/validation", true);
DefaultHandler myHandler = new DefaultHandler();
// use the same parser instance to parse XML documents
for (int i=0; i < args.length; i++){
   parser.parse(args[i], myHandler);
}
// reset parser features 
for (int i=0; i < currentFeatures.size(); i++){
   String feature = (String) currentFeatures.get(i);
   parser.getXMLReader().setFeature(
       feature, ((Boolean)defaultFeatureValues.get(feature)).booleanValue());
}

パーサ機能をリセットするための簡単なAPIは無く、場合によるとプロパティがリセットできないこともあります。例えば、Xerces2 version 2.6.2では、プロパティの値を null に設定しようとするとNullPointerException をスローします。こんなことなら、複数のパーサ・インスタンスを使った方が良いと言うことになります。

まず、パーサをプールするインターフェースを定義し、その実装をアプリケーションに登録します。機能やプロパティの条件が与えられると、パーサ・プールは内部のプールからパーサを返すか、あるいはパーサ・インスタンスが無い場合には、新しいパーサ・インスタンスを生成して保存します。アプリケーションがパーサを取得する必要がある場合に、アプリケーションはパーサ・プール実装とやり取りする必要があります。

アプリケーションがマルチ・スレッド環境で動作するのであれば、パーサ・プールが同期していることを確実にしておく必要があります。この場合にパーサ・プールは、getメソッドだけでなくreleaseメソッドも定義する必要があります。releaseメソッドによってスレッドがパーサ・インスタンスを解放してプールに戻せるようになり、他のスレッドがそのパーサ・インスタンスを使えるようにもなります。リスト4 は、SAXのパーサ・プールに対するインターフェースとして考えられるものの一例です。実装を確実にスレッド・セーフにするためには、このインターフェースを実装するクラスは、メソッドでsynchronizedキーワードを使用するか、メソッド内で同期化ブロックを使用する必要があります。

リスト4. マルチ・スレッド環境でSAXパーサを再利用するためのインターフェースの例
public interface XMLParserPool
{
  /**
   * Retrieves a parser from the pool given specified properties 
   * and features.
   * If parser can't be created using specified properties 
   * or features, an exception can be thrown.
   */
  public SAXParser get(Map features, Map properties) 
           throws ParserConfigurationException, SAXException;

  /**
   * Returns the parser to the pool.
   */
  public void release(SAXParser parser, 
            Map features, 
            Map properties);
}

まとめ

この記事では、Xerces2 SAXやDOM実装を使っている場合にアプリケーションのパフォーマンスを改善する方法について説明してきました。また、パーサを再利用したり、キャッシュしたりすることで、XMLアプリケーションのパフォーマンスを改善する方法についても説明しました。シリーズ第3回の記事では、Xerces2特有の機能やプロパティを使用してパフォーマンスを改善する方法に関する説明を続ける予定です。その中でXerces Native Interface (XNI) の概略を説明した上でSAXと比較し、DTDやXMLスキーマに対する妥当性の検証が必要なアプリケーションのパフォーマンスを大幅に改善するXerces2文法キャッシュAPIについて解説します。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML
ArticleID=243237
ArticleTitle=XMLアプリケーションのパフォーマンスを改善する 第2回
publish-date=07302004