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

XML文書を書き、SAXおよびDOM APIを使ってアプリケーションを開発する

最高のパフォーマンスが得られるようにアプリケーションを書くためにはどうすべきか、さらにSAXおよびDOMの操作と機能のうち、どれがアプリケーションのパフォーマンスに影響を与えるかを学びます。3回シリーズの第1回として、著者のElena LitaniとMichael GlavassevichがXMLアプリケーションやXML文書を書く上で、また標準的なSAXやDOMのAPIを使ってアプリケーションを開発する上でのベスト・プラクティスについて説明します。

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月 26日

今日、XMLはパフォーマンスが重大な意味を持つ多くのシナリオにおいて、重要な役割を担っています。XML文書やXMLスキーマ、DTDなどの書き方を知っている開発者が多いのに対して、XML文書を構成する上での選択事項や、XML文書を構文解析する前にどんな機能をパーサに設定するかによってXMLアプリケーションのパフォーマンスに影響する、ということを意識していない一部の開発者もいるようです。

多くの開発者はまた、どういう場合にSAXを使うべきで、どういう場合にDOM APIを使うべきかも知っているようです。一般的に言ってSAXを使う方が良いのは、メモリが懸念事項である場合、例えばアプリケーションが大きな文書を処理する必要がある場合や、メモリ上に(DOM以外の)アプリケーション自体の表現を作成する必要がある場合です。これに対して、DOMを使う方が良いのは、アプリケーションが文書データにランダムにアクセスして修正する必要がある場合や、複雑な検索を必要とする場合、また文書ツリーを何度もトラバースする必要がある場合などです。この記事では、SAXやDOMの操作、機能のうち、どれがアプリケーションのパフォーマンスに影響を与えるかを説明し、最高のパフォーマンスを得るためにはアプリケーションをどのように書けば良いかについて説明します。

XML文書を書く

XML文書を書く責任を担う開発者は、XMLアプリケーションのパフォーマンスを改善するために、様々な作業を行うことができます。

それぞれのXML文書では、XML宣言にて文字のエンコーディングを指定することができます。XML文書を書くにあたって最適なパフォーマンスを得るためには、エンコーディングにUS ASCII ("US-ASCII") を使用してください。ASCII文字を使って書かれた文書は、各文字が単一バイトであることが保証されており、しかも等価なUnicode値に直接マップできるため、最も高速な構文解析が可能です。文書がUTF-8でエンコードされていても含まれている文字がASCII文字のみの場合には、(Xerces2のような)一部のパーサでは、US-ASCIIでエンコードした等価なXML文書を処理する場合とほとんど同じ方法で実行されます。ASCIIの範囲を越えるUnicode文字を含む文書の場合には、パーサが各文字に対して、連続した複数バイトを読み込んで変換する必要があります。この変換がパフォーマンスの低下をもたらします。UTF-16エンコーディングでは、各文字が2バイトを使うと規定されており、サロゲート文字を想定していないので、パフォーマンスの低下が緩和されます。ただし、UTF-16を使うと元の文書のサイズがほぼ2倍となり、構文解析にかかる時間が長くなります。

また、文書中で使われる改行や、空白の数を減らすことでもパフォーマンスを改善することができます。開発者は通常、編集を行い易くするために、例えばキャリッジ・リターン(#xD)とライン・フィード(#xA)を使って、文書を行単位で整理しようとします。XMLパーサは、連続した2文字#xD #xA と、(#xAが後に続いていない)任意の#xD を、単一の#xA 文字に変換しなければなりません。この変換は無償ではありません。全体的な構文解析のパフォーマンスに及ぼす影響は、改行の数に比例した文書中の文字数に左右されます。これは空白についても当てはまります。文書に空白を追加すると、パーサはより多くの文字を処理することになり、結果的に構文解析のパフォーマンスに影響を及ぼします。

また、絶対に必要な場合以外、アプリケーションの中で名前空間を使うことは避けるべきです。名前空間の機能を使えるようにした文書の処理は、文書全体の処理速度を低下させます。パーサは、名前空間の宣言を処理し、その正当性を検証するだけではなく、XML文書が確実に名前空間整形式(namespace well-formed)であることも保証するのです。

妥当性検証を必要としないアプリケーションであれば、その文書の中に<!DOCTYPE...> 行を含むべきではありません。XMLの仕様によれば、Xerces2のような妥当性検証を行うプロセッサは、デフォルト属性、属性タイプなどに関する情報を得るために、内部や外部のDTDサブセットを処理しなければなりません。このようなプロセッサは、たとえ妥当性検証機能がオフになっていてもDTDを処理します。

アプリケーションが妥当性検証を必要とする場合に、DTDに対して処理や妥当性の検証を行う方が、W3C XML Schemaに対して処理や妥当性の検証をするよりも安くすむことを念頭に置いておく必要があります。さらに、ファイルを開いたり読んだりする操作は高くつくものなので、(外部DTDやXML Schemaのインポートなど)多くの外部実体を使うことは避けるべきです。また、デフォルト属性をたくさん使うと妥当性検証の時間を増大させることになるので、これも避けるべきです。同様に、XML Schemaのredefine構成やidentity制約も妥当性検証処理の所要時間に影響するため、避けた方が無難です。


SAXのパフォーマンスに関する一般的なヒント

DOMのように、より多くのメモリを消費するAPIに対して、SAXを選択するだけでアプリケーションのパフォーマンスを改善することができます。また、次のような意識を持つことによって、さらに効果を高めることができます。SAXアプリケーションのパフォーマンスを改善するこれらのヒントを試してみてください。

文字列の内部化

SAXの機能URIhttp://xml.org/sax/features/string-interning で示される機能を指定します。この機能が真(true)として設定されると、(例えば要素名や属性名などの)XML名と名前空間URIを、java.lang.String.intern()を呼び出すことによって取り込まれた(プールされた)内部化文字列として報告するように、パーサに対して指示します。

文字列の等価性を確認する速度を加速するために、この機能をオンにしてください。そうすれば一文字ずつ文字列を比較するequals()を呼ぶ代わりに、パーサが文字列定数に基づいて報告した名前を、参照によって比較することができます。パーサが報告するXML名をハッシュ・テーブルのキーとして使うことにより、そのテーブルがもしjava.lang.StringhashCodeメソッドを呼ぶのなら、内部化された文字列は参照時間を改善するはずです。Javadocには明示されていませんが、このhashCodeメソッドの実装は、一般的にオブジェクトの中のハッシュ・コード値を計算した後、その値をキャッシュします。ですから、ハッシュ・コードが一度計算されると、内部化された文字列は実質的に無償でハッシュ・コードを取得できることになります。

一部のパーサ実装では、文字列の内部化機能をサポートしていない場合があります。Xerces2では、高速な比較のために内部化文字列機能を使っているので、この機能は常にオンになっています。

コンテンツ・ハンドラを切り換える

大きなXMLボキャブラリを処理してみると、自分のコールバック・メソッドに膨大な数のif文やelse文があることに気がつきます。SAX仕様に書かれている通り、構文解析中にいつでも、新しいコンテンツ・ハンドラを登録することができます。文書中の別々の部分に異なったコンテンツ・ハンドラを使うことによって、コールバック・メソッドの複雑さや長さを減らすことができます。リスト2 に示すクラスは、リスト1 に示す文書の処理をどのようにして複数のハンドラに分割できるのかを示しています。

リスト1. XML文書の例
<?xml version="1.0" encoding="US-ASCII"?>
<!DOCTYPE root [
 <!ELEMENT root (child*)>
 <!ELEMENT child (#PCDATA)>
]>
<root><child/></root>
リスト2. 複数のコンテンツ・ハンドラを使う
public class MultipleHandlersExample {
    
    private XMLReader reader;
    private ContentHandler docHandler;
    private ContentHandler rootContentHandler;
    private ContentHandler childContentHandler;
        
    ...
    
    public void parse (String uri) throws SAXException, IOException {
        reader.setContentHandler(docHandler);
        reader.parse(uri);
    }
    
    public class DocHandler extends DefaultHandler {
        public void startElement(String uri, String localName, 
            String qName, Attributes atts) {
            if ("root".equals(qName)) {
                // process root
                reader.setContentHandler(rootContentHandler);
            }
            else {} // error: only root expected here 
        }
    }
    
    public class RootContentHandler extends DefaultHandler {
        public void startElement(String uri, String localName, 
            String qName, Attributes atts) {
            if ("child".equals(qName)) {
                // process child
                reader.setContentHandler(childContentHandler);
            }
            else {} // error: only child expected here
        }
        public void endElement(String uri, String localName, 
            String qName) {
            // end of root, set content handler for document
            reader.setContentHandler(docHandler);
        }
    }
    
    public class ChildContentHandler extends DefaultHandler {
        public void startElement(String uri, String localName, 
            String qName, Attributes atts) {
            // error: no element content expected here
        }
        public void endElement(String uri, String localName,
            String qName) {
            // end of child, set content handler for root
            reader.setContentHandler(rootContentHandler);
        }
    }
}

ある特定の要素(例えば上の例でのrootchild)が報告された場合、この要素の内容を処理するためのハンドラを登録することができます。そして、その要素の最後で、親要素のためにコンテンツ・ハンドラを復元します。リスト1 に示すものよりも複雑な文書の場合には、コンテンツ・ハンドラをスタックにプッシュしたり、スタックからポップしたりすることで、このようなことを行うことができます。こうして、要素の内容を処理することにより、それぞれのハンドラ・メソッドのコードはずっと小さくなります。このようにメソッドの長さを短くすると、JITコンパイラによる最適化がしやすくなります。

SAXパーサの構成によっては、アプリケーションは異なった動作をする必要があるかも知れません。例えば、文字列の内部化を行いたいとしてもパーサがその機能をサポートしない場合には、アプリケーションはequals()を使って文字列を比較する必要があります。両方の場合を処理する一つのコンテンツ・ハンドラを使うこともできますが、そうすると、パーサがハンドラを呼び出す度に、どちらの場合を処理する必要があるのかを確認する必要が出てきます。ですから、融通の利かないハンドラを一つ書く代わりに、文字列に対する参照比較を行うハンドラと行わないハンドラという、二つのコンテンツ・ハンドラを書けば良いのです。どちらのハンドラを使うかは、構文解析の前に決定します。

実体リゾルバで外部実体をロードする

外部DTDを参照するXML文書や、外部実体に対する参照を多く含むXML文書の処理は、非常にコストの高いものになります。パーサはこうした実体のそれぞれに対して、世界のどこかにあるリソースを探し出し、読み込む必要があります。もしこのリソースが自分のハード・ドライブ上にあれば、パーサはファイルをオープンしなければなりません。パーサが文字データに対して、(常に文字を内部的に16ビット単位(UTF-16)で表現するXerces2のように)内部的に単一のエンコーディングとして動作するのであれば、パーサはこうしたファイルのそれぞれをコード変換する必要があります。文書がネットワーク上または、インターネット上の実体への参照を含んでいる場合(そして、こうしたリソースに対して自分の環境からアクセスできる場合)、特に一回のネットワーク反応時間が長いような場合には、大きなパフォーマンス低下を我慢せざるを得なくなります。Xerces2を含む多くのパーサは、既に解釈した実体をメモリ内に保持しません。ですからその文書が実体を複数回参照している場合には、参照している回数だけパーサはその実体を読み出してきます。

XML文書に外部実体または外部DTDへの参照がある場合には、これらの実体を実体リゾルバでメモリ内にロードすることによって、アプリケーションのパフォーマンスを改善することができます。ですから、実体の内容が最初に読み込まれた時に実体の内容をキャッシュするように、実体リゾルバを書けば良いわけです。そうすればアプリケーションは、一つの実体に対して一度だけ検索のコストを負担すれば済むことになります。実体を動的にロードする必要がない場合には、メモリから読み出したい実体をアプリケーションに予めロードしておくことができます。実体をjava.lang.String としてメモリ内に保存することで、その実体のエンコーディングを文字に変換する際にパーサによって生じるコストを避けることができます。これをリスト3 に示します。

リスト3. 外部実体をメモリからロードする
public class MyEntityResolver implements EntityResolver {
    private String externalEntity = ...;
    InputSource resolveEntity(String publicId, String systemId)
        throws SAXException, IOException {
        if (systemId.equals("ExternalEntity.xml") {
            return new InputSource(new StringReader(externalEntity));
        }
        return null;
    }
}

外部実体を処理しないようにする

アプリケーションが処理するXML文書には外部実体に対する参照が含まれているかも知れませんが、それを展開する必要はないかも知れません。SAXでは、http://xml.org/sax/features/external-general-entitieshttp://xml.org/sax/features/external-parameters-entities という機能URIで示される二つの機能を定義しており、通常の外部実体や外部パラメータ実体をパーサが処理するかどうかを制御できるようになっています。こうした機能を使用不可にしておいた場合に、文書の処理中に外部実体の参照に行き当たると、SAXパーサは実体の内容は報告しませんが、その代わりコンテンツ・ハンドラのskippedEntityコールバックに対して実体の名前を報告するのです。外部実体の内容がアプリケーションに関係なければ、外部実体を処理しないように、こうした機能をオフすることができます。


DOMのパフォーマンスに関する一般的なヒント

DOMは、ElementAttributeのように、幾つかの型のノードを定義します。あるノード型に基づいて特定の操作をするようなコードを書く場合には、ノード型のチェックにJavaのinstanceof演算子を使わないようにします。処理対象のノードの型を検出するには、代わりにgetNodeTypeNodeインターフェース)メソッドを使います。

属性のリストを検出する前には必ず、ノードに属性があるかどうかを、hasAttributesメソッドを使って照会するようにします。ノードに属性がある場合には、そのノードをElementノードにキャストし、getAttributesメソッドを使って属性のリストを検出します。この操作手順により、Elementノードに対して不必要なNodeへのキャストや、空のNamedNodeMapを作ってしまったりすることが避けられるようになります。getAttributesメソッドは、ノードに何も属性がない場合でも常にマップを返すのです。

アプリケーションで属性Nodeを照会したり修正したりする必要がある場合には、hasAttribute(String)メソッドやhasAttributeNS(String, String)メソッドは使わないようにします。その代わりにgetAttribute(String)メソッド、またはgetAttributeNS(String, String)メソッドを使って属性Nodeを検出するようにすれば、照会や修正ができるようになります。

DOM APIにある操作のうちの、幾つかは非常に高くつきます。importNode操作は、結果として新しいノードを生成することになるので、代わりにadoptNode が使えないかを検討すべきです。getElementByTagNamegetElementByTagNameNS のどちらも、名前や名前空間URIの比較にJavaのString.equals() メソッドを使い、DOMツリーをトラバースしながらノードを探します。ですからそれを使う代わりに、アプリケーションで(文字列が内部化されている場合)文字列の比較にJavaの== を使う独自のトラバーサル・メソッドを書くようにするか、あるいはツリーの一部分のみを検索するようにすることができます。同じことがgetElementByIdメソッドについても言えます。

さらに、DOM Level 3のnormalizeDocumentメソッドは、注意して使うようにしてください。このメソッドを使うことで妥当性検証のパフォーマンスは改善するのですが、そのコストも非常に高くなります。例えば、このメソッドはデフォルトで、ツリーが名前空間整形式であることを保証します。つまりこのメソッドは、属性や要素に対して必要な名前空間宣言が全て追加されたかどうかを確認し、必要な場合には一部の属性や要素の接頭辞を変更するかも知れないのです。ツリーが名前空間整形式であることをアプリケーション・コードで既に確認しているならば、アプリケーションはnormalizeDocumentメソッドを呼び出す前に、DOMConfigurationインターフェースを使って「名前空間」パラメータをオフにすべきです。これは、デフォルトで真(true)である、整形式パラメータにも当てはまります。通常は、DOMツリーが整形式であることを忘れないでください。その例外となるのは、開発者がCommentノードに-- を含めたり、CDATASectionノードに]]> を含めたり、あるいは(CDATAセクションやコメントを含めた)原文の内容に非XML文字を使ったりした場合です。ですから、たいていのアプリケーションでは、normalizeDocument を呼ぶ際に、整形式パラメータを使用不可にしても安全であり、それがこのメソッドのパフォーマンスに大きく影響します。

DOM Level 3 API

DOM Level 3のコア仕様やロード、セーブ仕様では、DOMアプリケーションのパフォーマンスを改善するような幾つかの新しい操作と一つのAPIが定義されています。ですから、自分のDOMアプリケーションを、J2SE 5.0でサポートされる DOM Level 3 を使うように移行して行くことを考えるべきでしょう。

ノードのリネームと移動

DOM Level 2では、ノードをリネームしたり、一つの文書から別の文書にノードを移動したりすることは比較的高くつくものでした。これは、新しいノードを作り、そのノードの内容をコピーし、そしてツリーの適切な場所にノードを挿入することを伴うためです。こうした操作のパフォーマンスを改善するには、renameNodeメソッドやadoptNodeメソッドを使うようにアプリケーションを書くことです。通常、renameNodeメソッドは、ノードの名前を変更するだけです。ただし稀ですが場合によっては、このメソッドでも新しいノードを作り、情報をすべてコピーし、新しいノードをツリーに挿入することになるかも知れません。Xerces2 DOMでこれが起きるのは、名前空間を認識しないノード(つまりcreateElement などのようなDOM Level 1のメソッドで作られたノード)をアプリケーションが作り、後からこのノードをリネームして名前空間URIを追加しようとする場合のみです。Xerces2では、名前空間を認識するノード(例えばcreateElementNS などのようなDOM Level 2のメソッドで作られたノード)と名前空間を認識しないノードに対して別のクラスを使用するため、Xerces2のDOM実装ではrenameNode の操作の中でノードの新しいインスタンスを作らざるを得ないようになります。先に説明した通り、アプリケーションが一つの文書の中で名前空間を認識するノードと認識しないノードを混在させようとする時に生じるのがほとんどなので、これは稀な場合ということができます。二種類のノードを混在させると(ツリーの妥当性検証などにおいて)不測の結果につながりかねないため、極力避けるようにします。

メモリ内で妥当性を検証する

DOM Level 3を使用すれば、メモリ内でDOMの妥当性を検証できます。今までは、スキーマに対して妥当性を検証しようとすると、非常に複雑な妥当性検証コードを自分で書くか、あるいはDOMをシリアル化してから妥当性検証ができるパーサを使ってメモリにロードし直すという、いずれかしかありませんでした。normalizeDocumentメソッドを使えば、コストのかかる他の操作を避け、一つの簡単な手段でDOMの妥当性検証を行うことができます。normalizeDocument の使い方については、記事「Discover key features of DOM Level 3 Core, Part 2」を読んでください。

不必要なチェックを避ける

通常DOMの実装は、操作の正しさを検証する必要があり、アプリケーションが間違ったパラメータを渡したり、不正な操作を行ったりした場合には、例外を投げる必要があります。例えばDOMの実装は、createElementNSメソッドに対して、qualifiedName が QName(参考文献参照)の定義に従っていることを検証する必要があります。DOM Level 3ではDocumentインターフェースに新しいstrictErrorChecking属性を追加しています。(例えば、DOMツリーがSAXイベントを使って構成された場合など・・・)アプリケーションがDOMに対して行う全ての操作が正当なものだと確信できるのであれば、厳密なエラー・チェックをオフにすることで、パフォーマンスを改善することができます。

ロードとセーブにフィルタAPIを使う

DOM Level 3では、文書の構造を変更する前に構文解析が終了するのを待つ必要がありません。新しいフィルタAPIを使うと、ノードを受け付けるか、スキップするか、あるいはノードとその子供を生成されるツリーから削除するようにパーサに要求することによって、構文解析中に文書の構造を検査したり、修正したりすることができます。また、フィルタAPIを使って構文解析を中断することで、XML文書の一部のみをロードするように選択することもできます。こうした構文解析中の文書修正によって、DOMツリーによるメモリ消費量が小さくなり、メモリ中で文書をトラバースしながら修正するための時間も減らすことができます。

シリアル化フィルタを使うことによって、どのノードをXMLにシリアル化するかを、元のDOMツリーを修正することなくアプリケーションが指定できるようになります。これによって、同じDOMツリーを複数のXML文書にシリアル化するような柔軟性をもたらします。また、DOMツリーをトラバースしたり、修正したりするという、コストのかかりがちな操作を避けられるようにもなります。


まとめ

この記事では、XMLアプリケーションのパフォーマンスをどのように改善するかについて説明しました。最初に構文解析のパフォーマンスの改善を実現するXMLの書き方を解説し、次にSAXやDOMアプリケーションのパフォーマンスの改善方法を説明しました。このシリーズ2回目の記事では、Xerces2実装を使っている場合にSAXアプリケーションやDOMアプリケーションのパフォーマンスをどのように改善するかについて説明する予定です。また、パーサ・インスタンスの再利用方法についても紹介します。

参考文献

  • W3CによるDocument Object Model (DOM) 仕様を読んでください。
  • SAX についてさらに知識を得てください。
  • Xerces2 パーサについて学んでください。
  • Discover key features of DOM Level 3 Core, Part 1」(developerWorks, 2003年8月)を読んでください。この記事ではDOM Level 3仕様の重要な機能の幾つかについて説明しています。「Part 2」(developerWorks, 2003年8月)では、文書に対する操作や型情報へのアクセスについて説明し、Xerces2のDOM実装を紹介しています。
  • Qualified Names についてよく調べてください。
  • developerWorksのXMLゾーン には、広範囲な話題の記事やコラム、チュートリアル、ヒントなどが取り揃えられています。
  • XMLに関するこうした書籍 を含め、developerWorksのDeveloper Bookstore にはXML関連の書籍が豊富に揃っていますので、ご覧下さい。
  • XMLおよび関連技術においてIBM認証開発者になる方法についてはこちら を参照してください。

コメント

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=243236
ArticleTitle=XMLアプリケーションのパフォーマンスを改善する 第1回
publish-date=07262004