Chris Lovett
Microsoft Corporation
February 21, 2000
日本語版最終更新日 2000 年 11 月 30 日
MSDN Code Center からこのコラムのサンプル コードをダウンロードする。
次の記事は、MSDN Online Voices の 「 Extreme XML」 コラムに掲載されたものです。
読者からのオンライン コメントでは、「初心者レベル」の内容と、本物の XML アプリケーションが必要だという声が寄せられています。しかし、この記事はすでに進行中だったので、上級の XML 開発者を対象としています (結局のところ、このコラムには "Extreme XML" という名前が付いているのです !)。そういうわけで、この記事は XML と、特に Microsoft XML パーサー (MSXML) に関する知識を前提としています。詳細については、MSDN XML Developer's Center を参照してください。
XML ベースの Web アプリケーションを設計していて、XML サーバーにどれほどのパフォーマンスが期待できるのかを知りたいとしましょう。明らかに、これはどのような処理を行う予定なのかに依存します。XML ドキュメントのサイズ、ドキュメントの処理に必要なスクリプト コードの量、生成される出力の量など、変数が多すぎるため、一般化して論じるのは困難です。
たとえば、MSXML のパフォーマンスに影響を与えうる主な変数には以下のようなものがあります。
- XML データの種類
- タグとテキストの比率
- 属性と要素の比率
- 破棄される空白の量
これらの変数の例を示すために、4 つのサンプル データ ファイルを使用します。以下に示すのは、個々のファイルの内容を把握するための抜粋です。
Ado.xml
このサンプル ファイルは、ADO Recordset オブジェクトを永続化したものが保存されており、属性の比率がきわめて高くなっています。個々の属性値は短く、無駄な空白はほとんどないため、データの密度が高いドキュメントとなっています。
<rsSchema:row au_id='267-41-2394' au_lname='O'Leary' au_fname='Michael'
phone='408 286-2428' address='22 Cleveland Av. #14' city='San Jose' state='CA'
zip='95128' contract='True' name='systypes' id='4' uid='1' type='S ' userstat='0'
sysstat='113' indexdel='0' schema_ver='1' refdate='1900-01-01T00:00:00'
crdate='1996-04-03T03:38:57.387000000' version='0' deltrig='0' instrig='0'
updtrig='0' seltrig='0' category='0' cache='0'/>
Hamlet.xml
このサンプル ファイルは、シェイクスピアの戯曲『ハムレット』から構成されています。ファイルはテキストと要素マークアップのバランスのとれた組み合わせで、属性は含まれていません。
<SCENE><TITLE>SCENE I. Elsinore. A platform before the castle.</TITLE>
<STAGEDIR>FRANCISCO at his post. Enter to him BERNARDO</STAGEDIR>
<SPEECH>
<SPEAKER>BERNARDO</SPEAKER>
<LINE>Who's there?</LINE>
</SPEECH>
Ot.xml
このサンプル ファイルは、旧約聖書全文から構成されています。個々のタグは 1 文字か 2 文字で、タグとテキストの比率は低くなっています。
<book>
<bktlong>The First Book of Moses, Called GENESIS.</bktlong>
<bktshort>Genesis</bktshort>
<chapter><chtitle>Chapter 1</chtitle>
<v><vn>1</vn><p>In the beginning God created the heaven and the earth.</p></v>
...
Northwind.xml
このサンプル ファイルは、Microsoft Access に付属している ノースウィンド データベースの一部を含んでいます。属性の代わりに要素を使用し、タグとテキストの比率が高くなっていて、余分な空白が大量に含まれています。
<OrderIDs>
<Item>
<OrderID> 10326</OrderID>
<OrderDate> 11/10/94</OrderDate>
<ShipAddress> C/ Araquil, 67</ShipAddress>
</Item>
...
もう 1 つの重要な要因が、元のファイルが UCS-2 として格納されているかどうかです。英語のほとんどの XML ドキュメントでは、UTF-8 は UCS-2 の半分のサイズになります。ラテン文字は UTF-8 では 1 バイトに圧縮されるからです。しかし、これはすべての言語に当てはまるわけではありません。たとえば、アジア諸国の言語では、UTF-8 は実際に UCS-2 よりも大きく、最悪のケースでは 1 文字が 3 バイトに拡張されます。パフォーマンスの計測を公正に行うためには、グローバルに意味のある数値を得るために、UCS-2 を使って計測を行うべきでしょう。
次の表は、個々のサンプル ファイルについて、UCS-2 ファイルのサイズ、ユニーク名の数、要素と属性の数、テキスト ノードの数、およびテキスト コンテンツの量 (Unicode 文字の数) を示しています。また、要素と属性名の文字と、ファイルのその他の内容の比率である「タグ係数」も示しています。
| サンプル | ファイル サイズ | ユニーク名 | 要素と属性 | テキスト ノード | テキスト コンテンツ (文字数) | タグの量 (パーセンテージ) |
|
Ado.xml
|
2,171,812
|
53
|
63,722
|
61,462
|
3890
|
18.7
|
|
Hamlet.xml
|
559,260
|
17
|
6637
|
5472
|
170,545
|
5.9
|
|
Ot.xml
|
7,663,624
|
12
|
71,417
|
47,302
|
3,236,900
|
1.4
|
|
Northwind.xml
|
488,140
|
12
|
3680
|
2761
|
31,155
|
6.0
|
ユニーク名の数は興味深い要因です。というのも、MSXML は要素名と属性名を「アトム化」し、個々のユニーク名について 1 つの文字列オブジェクトのみを作成して、同じ名前を共有するすべての要素または属性からそのオブジェクトをポイントするようにしているからです。これが重要なのは、要素と属性の名前が一般に繰り返されることが多いからです。たとえば、Ado.xml サンプルには 63,722 の要素名と属性名が含まれており、合計でファイル サイズのうちの 407,148 バイトを占有しています。タグとファイル サイズの比率は 18% を超えています ! しかし、これらの名前のうち、ユニーク名は 53 個しかありません。このため、これらの名前は 407 KB ではなく数キロバイトしか占有しません。
メトリックス
XML ベースの Web アプリケーションの開発者のほとんどにとって重要なパフォーマンス メトリックスが 4 つあります。
注意: ここで使っている数値は、公式なものではなく、どの程度のパフォーマンスが得られるのかというおおよその感触をつかみ、XML アプリケーションを構築する際に適切な設計上の決定を下せるようにするためのヒントであることに注意してください。
MSXML の機能
次に、Document Object Model (DOM) に関連するいくつかの重要なシナリオを検討します。これには、DOM ツリーのロード、保存、移動、そしてメモリ内での新しい DOM ツリーの作成が含まれます。
DOM
MSXML Document Object Model ("Microsoft.XMLDOM," CLSID_DOMDocument, IID_IXMLDOMDocument) は、MSXML パーサーの中でのすべての XML 処理の出発点となります。XML ドキュメントをロードする最も高速な手段は、validateOnParse、resolveExternals、および preserveWhiteSpace をすべて無効にして、既定の 「レンタル」 スレッディング モデルを使用する方法です (このスレッディング モデルでは、DOM ドキュメントは一度に 1 つのスレッドからしか使用できません)。
var doc = new ActiveXObject("Microsoft.XMLDOM");
doc.validateOnParse = false;
doc.resolveExternals = false;
doc.preserveWhiteSpace = false;
doc.load("test.xml");
ワーキング セット
DOM を使用するときに、最初に考慮しなければならないメトリックがワーキング セットです。メモリは、Msxml.dll と、これが依存している他の.dll ファイルをロードするために使用されます。一部の.dll ファイルは「遅延ロード」されます。つまり、その.dll が使用されるまで、ワーキング セットには影響は及びません。MSXML は COM DLL なので、通常は標準の COM API (CoInitialize と CoCreateInstance) を使って新しい XML ドキュメント オブジェクトを作成することになります。COM を使用する単純な Visual C++ 6.0 コマンド ライン アプリケーションの最小ワーキング セットは約 1 メガバイトです (これには、Ntdll.dll、Kernel32.dll、Ole32.dll、Rpcrt4.dll、Advapi32.dll、Gdi.dll、User32.dll、および Oleaut32.dll の各.dll ファイルが含まれます)。IXMLDOMDocument オブジェクトの CoCreateInstance の最初の呼び出しによって、Msxml.dll と Shlwapi.dll がロードされ、さらに 745 KB が追加されます。いったんすべての.dll ファイルがロードされたら、新しい IXMLDOMDocument オブジェクトは約 8 KB しか占有しません。
XML ドキュメントにロードされた XML データが使用するメモリは、ロードされたデータの 「タグの量」 と、ファイルがディスク上ですでに Unicode 形式になっていたかどうかによって、ディスク上の XML ファイルのサイズの 1~4 倍になります。次に、特定の XML ドキュメントに必要なメモリを推定するためのきわめて大まかな計算式を示します。
ws = 32(n+t) + 12t + 50u + 2w;
次に表に、この計算式の各項の意味を示します。
| 項 | 説明 |
|
ws
|
ワーキング セットのバイト数。
|
|
n
|
ツリーの中の要素および属性ノードの数。個々の要素、属性、属性値、およびテキスト コンテンツは 1 つのノードを持っています (たとえば、<element attribute = "value">text</element> は 4 つのノードを持ちます)。
|
|
t
|
テキスト ノードの数。
|
|
u
|
ユニークな要素および属性名の数。
|
|
w
|
テキスト コンテンツの中の Unicode 文字の数 (属性値を含む)。シングルバイトの ANSI テキストをメモリにロードすると、すべてのテキストが 2 バイトの Unicode 文字として格納されるので、サイズが 2 倍に増えることに注意してください。
|
これは、preserveWhiteSpace フラグを設定しなかった場合です。このフラグを設定すると、要素間の空白を保存するために、より多くのノードが作成され、メモリの使用量は増えます。
前に示したサンプル データのワーキング セットは以下のようになります (初期のスタートアップ ワーキング セットを除く)。
| サンプル | ワーキング セット | ファイルを基準とした比率 |
|
Ado.xml
|
4,689,920
|
2.16
|
|
Hamlet.xml
|
704,512
|
1.25
|
|
Ot.xml
|
10,720,000
|
1.39
|
|
Northwind.xml
|
249,856
|
0.51
|
要素間に多数の空白を含んでおり、Unicode で格納されている、多数の要素を含んだ XML ドキュメントは、メモリ内のサイズの方がディスク上のサイズよりも小さくなることがあります。Hamlet.xml や Ot.xml のように、要素とテキスト コンテンツの比率のバランスが取れているファイルは、メモリ内では UCS-2 ファイル サイズの 1.25~1.5 倍になります。Ado.xml のようにデータの密度が高いファイルは、メモリにロードすると、ディスク ファイルのサイズの 2 倍以上になります。
メガバイト/秒
メガバイト/秒のメトリックについては、Windows 2000 を実行している Pentium II 450-MHz 2 プロセッサ コンピュータ上で個々のサンプル ファイルをループを使って 10 回ロードし、ロード時間を計測して、結果の平均値を計算しました。
| サンプル | ロード時間 (ミリ秒) | MB/秒 | ノード/秒 |
|
Ado.xml
|
677
|
3.2
|
184,909
|
|
Hamlet.xml
|
104
|
5.3
|
116,432
|
|
Ot.xml
|
1063
|
7.2
|
111,682
|
|
Northwind.xml
|
62
|
7.8
|
103,887
|
この表には、ノード/秒の計測値も示しています。この値とメガバイト/秒の関係に注目してください。入力データのバッファ1 つで処理されるノードの数が多いほど、絶対スループットは低くなります。逆に、(Ado.xml のように) ノードがコンパクトであるほど、ノード/秒の値も大きくなります。
属性と要素
このことから、(Ado.xml のように) 属性が多数含まれている形式では、要素が多数含まれている形式よりも 1 秒当たりのデータ量が多くなるという結論を出すことができます。しかし、だからといってすべてのものを属性に変更するべきではありません。属性と要素のどちらを使用すべきかという決定を下す際には、他にもさまざまな要因を考慮に入れる必要があります。
最初の DOM ウォークのワーキング セット デルタ
DOM ツリーを最初にウォークする作業は、ツリー内の一部のノードはオンデマンドで作成されるため、ワーキング セット メトリックに影響を与えます。例として、新しくロードされたサンプル データ上での最初のウォークの結果として生じるワーキング セット デルタを示します。
| サンプル | ワーキング セット デルタ (パーセンテージ) |
|
Ado.xml
|
0
|
|
Hamlet.xml
|
25
|
|
Ot.xml
|
14
|
|
Northwind.xml
|
36
|
これらの結果を見ると、ADO の属性のケースでは、オンデマンドで作成されるノードはなかったことがわかります。
createNode のオーバーヘッド
DOM ツリーをゼロから作成すると、同じドキュメントをディスクからロードしたときよりも、ピーク時のワーキング セットが大きくなります。例として、次の内容の要素を 10,000 個含んだ DOM ドキュメントを作成しました。
<item>this is a test</item>
ロード時間、ロードとウォークに要した時間 (オンデマンドの作成を強制するため)、および作成時間を比較し、それぞれのワーキング セットを示したのが次の表です。
| 機能 | 時間 (ミリ秒) | ワーキング セット (バイト) |
|
ロード
|
146
|
842,137
|
|
ロード + ウォーク
|
148
|
1,173,914
|
|
作成
|
740
|
2,503,066
|
これらの結果から、ドキュメントのロードは、同じドキュメントをメモリ内でゼロから作成するのと比べると、約 5 倍の速度であることがわかります。これは、ドキュメントの作成には多数の DOM 呼び出しが必要になるためです。ドキュメントをロードするだけならば、これらの呼び出しをバイパスして、内部データ構造を直接読み込むことができます。
ウォークと selectSingleNode
ツリーを最速でウォークするためには、子コレクションと、あらゆる種類の配列アクセスを避けるようにします。その代わりに、firstChild と nextSibling を使用します。
function WalkNodes(node)
{
var child = node.firstChild;
while (child != null)
{
WalkNodes(child);
child = child.nextSibling;
}
}
次の表は、サンプル テスト ファイルで、すべての要素、属性、およびテキスト ノードをウォークした結果を示しています。
| サンプル | ウォーク時間 (ミリ秒) | ノード数 | ノード/秒 |
|
Ado.xml
|
243
|
63,723
|
262,234
|
|
Hamlet.xml
|
63
|
12,100
|
192,063
|
|
Ot.xml
|
660
|
118,720
|
179,878
|
|
Northwind.xml
|
33
|
6,438
|
195,090
|
ただし、ツリーの中の特定のものを探しているときには、selectSingleNode または selectNodes メソッドを通して、XPath を使って検索する方がはるかに高速です。例として、"//willnotfindanything" という XPath 式を入力してツリー全体をウォークしたときと、強制的なツリー ウォークとを比べたときのパフォーマンスの改善率を次の表に示します。
| ファイル | selectSingleNode (ミリ秒) | 改善率 (パーセンテージ) | ノード/秒 |
|
Ado.xml
|
173
|
29
|
368,341
|
|
Hamlet.xml
|
11
|
82
|
1,100,000
|
|
Ot.xml
|
113
|
82
|
1,050,619
|
|
Northwind.xml
|
5
|
84
|
1,287,600
|
selectSingleNode
が高速なのは、COM 層を通しての呼び出しのオーバーヘッドがないためです。firstChild と nextSibling を何千回も呼び出す代わりに、1 つの呼び出しで済ませています。
ところで、これは厳密に言えば対等な比較ではありません。selectSingleNode は、テキスト ノードを調べる必要がなく、すべてスキップできるので、ウォーキングの量が少なくなっています。その一方で、selectSingleNode は、個々の nodeName をクエリで指定されていた名前と比較しているため、より多くの作業を行っています。しかし、おおよその様子は理解できたでしょう。一般論として、selectSingleNode を使えば、作成しなければならない DOM コードの量が減り、パフォーマンスも顕著に改善されます。
保存
ドキュメントの保存は、一般にロードよりも低速です。次の表にその違いを示します。
| サンプル | ロード時間 (ミリ秒) | 保存時間 | 違い (パーセンテージ) |
|
Ado.xml
|
677
|
1,441
|
113
|
|
Hamlet.xml
|
104
|
184
|
77
|
|
Ot.xml
|
1,063
|
1,971
|
85
|
|
Northwind.xml
|
62
|
103
|
66
|
サンプルの保存の最悪のケースは、属性が多数含まれている ADO Recordset を保存した場合で、メモリへのロードの 2 倍の時間がかかります。しかし、書き込み操作が読み取り操作よりも遅いのは当然だということを考えると、これはそれほど悪い数値ではありません。ただし注意してください。「フリースレッド ドキュメント」のセクションで示すように、多数の小さい XML ドキュメントのロードと保存を行っていると、ファイル システムに簡単に負荷がかかってしまいます。
名前空間
XML 名前空間
も XML の解析時間にある程度のオーバーヘッドをもたらしますが、それは思ったより軽いものです。ここでは、Ado.xml を使って 2 つの例を示します。1 つは名前空間プレフィックスの "recordset:" を 2,347 のすべての行で使用しており、もう 1 つはまったく使用していません。次の表は、ロード時間の違いを示しています。
| 計測値 |
Ado.xml | Ado-ns.xml |
違い (パーセンテージ) |
|
ファイル サイズ
|
1,007,214
|
1,030,651
|
2.3
|
|
ロード時間 (ミリ秒)
|
662
|
680
|
2.7
|
ロード時間の違いの大部分は、ファイル サイズの増大によって説明できます。
フリースレッド ドキュメント
「フリースレッド」の DOM ドキュメント (CLSID_DOMFreeThreadedDocument、"Microsoft.FreeThreadedXMLDOM") は、「レンタル」スレッド ドキュメント (IID_IXMLDOMDocument) と同じインターフェイスを公開します。このオブジェクトは、同じプロセス内の任意のスレッド間で安全に共有することができます。
フリースレッド ドキュメントは、スレッド セーフティを実現するために必要な余分な作業のために、レンタル ドキュメントよりも一般に処理が遅くなります。これは、複数のスレッドで同時にドキュメントを共有し、各スレッドが独自のコピーをロードしないで済むようにしたい場合に使用します。一部のシナリオでは、スレッド セーフティを実現するために必要な余分な作業を上回るパフォーマンスの向上が見られます。
たとえば、Web サーバー上に 2 KB の XML ファイルがあり、そのファイルをロードし、ファイル中の属性をインクリメントし、再びファイルを保存する単純な ASP ページがあったとします。
<%@ LANGUAGE=JSCRIPT %>
<%
Response.Expires = -1;
var filename = Server.MapPath("simple.xml");
Application.Lock();
var doc = Server.CreateObject("Microsoft.XMLDOM");
doc.async = false;
doc.load(filename);
var c = parseInt(doc.documentElement.getAttribute("count"))+1;
doc.documentElement.setAttribute("count",c);
doc.save(filename);
Application.UnLock();
%>
<%=c%>
この ASP コードは、完全にディスク I/O の制約を受けます。私の Pentium II 450-MHz 2 プロセッサ コンピュータでは、CPU の使用量が 50% を超えることはありませんでした。一方、ディスクは騒々しい音を立てていました。
しかし、次のようにフリースレッド DOM ドキュメントを使用すれば、ファイルを共有アプリケーション状態に取り込むことができます。
<%@ LANGUAGE=JSCRIPT %>
<%
Response.Expires = -1;
var doc = Application("shared");
if (doc == null)
{
doc = Server.CreateObject("Microsoft.FreeThreadedXMLDOM");
doc.async = false;
doc.load(Server.MapPath("simple.xml"));
Application("shared") = doc;
}
Application.Lock();
var c = parseInt(doc.documentElement.getAttribute("count"))+1;
doc.documentElement.setAttribute("count",c);
Application.UnLock();
%>
<%=c%>
こうすれば、スループットは劇的に向上します。
このように、この 2 つ目のフリースレッド DOM ドキュメントを使用するアプローチは、1 つ目のアプローチの 7 倍も高速なのです !
遅延メモリ クリーンアップ
フリースレッド モデルの欠点の 1 つは、未使用のメモリのクリーンアップにおける待ち時間が増大し、それ以降の操作のパフォーマンスに影響を及ぼすということです (実際にはクリーンアップが遅れているだけなのに、これをメモリ リークとして報告してくる人もいます)。XML ドキュメントが大きいほど、待ち時間は大きくなります。次の表は、フリースレッド ドキュメントを使用したときに、レンタル ドキュメントと比べてロード時間とワーキング セットがどれほど増えるかを示しています。
| サンプル | ロード時間 (増加率のパーセンテージ) | ワーキング セット (増加率のパーセンテージ) |
|
Ado.xml
|
4
|
137
|
|
Hamlet.xml
|
23
|
83
|
|
Ot.xml
|
1
|
53
|
|
Northwind.xml
|
2
|
42
|
これらの結果から、最悪のケースは (Ado.xml のように) 多数のノードがある場合に生じることがわかります。これは、メモリ マネージャがクリーンアップしなくてはならないメモリが多くなるからだと考えられます。ただし、上に示した、同じドキュメント オブジェクトを異なるスレッド間で共有できることの利点は、ロード時間とワーキング セット サイズの増加を依然として上回っていることに注意してください。
仮想メモリ
フリースレッド モデルでは、メモリ マネージャがクリーンアップしなければならないメモリが大量に生成されると、ワーキング セットがスパイク状に増大する可能性があります。これは、大きなドキュメントに対して XSL 変換を実行するときに起こることがあります。この際には、使用可能なメモリがすべて消費され、仮想メモリ マネージャに負荷がかかって、パフォーマンスが劇的に低下します。アプリケーションに高い負荷がかかったときのピーク時のワーキング セットを監視して、このような状況が起こらないことを確認してください。このような状況が起こる場合には、アプリケーションを再設計して、XML を小さく分割します。この問題は、MSXML January 2000 Web Release ではある程度改善されました。実際、January 2000 Web Release ページには次のような読者からのコメントが寄せられています。
高速化 !
私は、サーバー サイド変換 (XSL 経由で XML から HTML へ) について、何度か非公式的な速度測定を行いましたが、速度が大幅に改善されていることがわかりました。
- 匿名
2000 年 2 月 9 日
IDispatch
JScript や VBScript などの遅延バインドを行うスクリプティング言語では、DOM インターフェイスの個々のメソッド呼び出しとプロパティ アクセスに大きなオーバーヘッドが生じます。スクリプト エンジンは、実際には IDispatch インターフェイスを通して、メソッドとプロパティを間接的に呼び出します。まず、両スクリプト エンジンは GetIDsOfNames または GetDispID を呼び出し、メソッドまたはプロパティの文字列名を渡して、DISPID を受け取ります。その後、エンジンはすべての引数を配列にパッケージ化し、DISPID を指定して Invoke を呼び出します。
これ以上の説明は不要でしょう。これは、C++ やコンパイルされた Visual Basic の仮想関数の呼び出しよりも、明らかに遅くなります。ただし Visual Basic では、1 つのアプリケーションで両方のプログラミング スタイルを使用することができるので、いくぶんトリッキーになります。次に例を示します。
Dim doc as Object
set doc = CreateObject("Microsoft.XMLDOM")
...
これは遅延バインドであり、VBScript や JScript と同じほど遅くなります。これを高速化するには、[プロジェクト] メニューから [参照設定] を選択し、最新バージョンの "Microsoft XML" ライブラリへの参照を追加します。その後、次の事前バインドのコードを書くことができます。
Dim doc As New MSXML.DOMDocument
...
このようなプログラミングのもう 1 つの利点は、Visual Basic で、使用可能なメソッドとその引数に関する情報を表示する便利なドロップダウン リストを使えるようになるということです。
検証
検証は、XML ドキュメントの中の要素のタイプを、Document Type Definition (DTD) または XML スキーマと比較照合します。たとえば、DTD で、すべての "Customer" 要素が子の "Name" 要素を含んでいなくてはならないと指定されているかもしれません。Hamlet.xml の DTD (hamletdtd.htm) と、Hamlet.xml の XML スキーマ (hamletschema.htm) を参照してください。
検証はパフォーマンス分析の重要な分野の 1 つですが、この記事では簡単に言及するだけにしておきます。検証にはいくつかの理由から大きなコストがかかります。第 1 に、別のファイル (DTD または XML スキーマ) をロードし、それをコンパイルしなくてはなりません。第 2 に、検証そのものを実行するために状態機械の操作が必要となります。第 3 に、スキーマにデータ型に関する情報が含まれている場合には、それらのデータ型に対しても検証を行わなくてはなりません。たとえば、XML 要素または属性が整数として入力された場合には、そのテキストを解析して、それが有効な整数であるかどうかを確認する必要があります。
次の表は、検証なしでのロード、DTD 検証を使ったロード、および XML スキーマの検証を使ったロードの違いを示しています。
| サンプル | ロード時間 (ミリ秒) | DTD (ミリ秒) | スキーマ (ミリ秒) | スキーマとデータ型 (ミリ秒) |
|
Ado.xml
|
662
|
2,230
|
2,167
|
3064
|
|
Hamlet.xml
|
106
|
215
|
220
|
N/A
|
|
Ot.xml
|
1,069
|
2,168
|
2,193
|
N/A
|
|
Northwind.xml
|
64
|
123
|
127
|
N/A
|
要するに、検証を行うと、ドキュメントのロード時間が 2 倍または 3 倍になることを覚悟する必要があります。MSXML January 2000 Web Release の新機能である SchemaCollection オブジェクトを使うと、XML スキーマを 1 回だけロードして、検証のためにドキュメント間で共有することができます。これについては、今後の記事で説明します。
XSL
XSL は、XML ドキュメントから「変換済み」レポートを生成する際には、DOM コードを使った場合よりもパフォーマンスを大幅に向上させることができます。たとえば、サンプル Hamlet.xml の中のハムレットのすべてのセリフをプリントアウトしたいとします。この場合には、selectNodes を使ってハムレットのすべてのセリフを検索し、別の selectNodes 呼び出しを使ってそれらのセリフの個々の行を繰り返し処理することができます。
function Method1(doc)
{
var speeches = doc.selectNodes("/PLAY/ACT/SCENE/SPEECH[SPEAKER='HAMLET']");
var s = speeches.nextNode();
var out = "";
while (s)
{
var lines = s.selectNodes("LINE");
var line = lines.nextNode();
while (line)
{
out += line.text;
line = lines.nextNode();
}
out += "<hr>";
s = speeches.nextNode();
}
return out;
}
これでうまく行きますが、約 1,500 ミリ秒かかります。この問題には、XSL の方が適しています。次の XSL スタイル シート (またはテンプレート) は、まったく同じ処理を行います。
<xsl:template xmlns:xsl="http://www.w3.org/TR/WD-xsl">
<xsl:for-each select="/PLAY/ACT/SCENE/SPEECH[SPEAKER='HAMLET']">
<xsl:for-each select="LINE">
<xsl:value-of/>
</xsl:for-each>
<hr/>
</xsl:for-each>
</xsl:template>
その後、このテンプレートを使用する、次の単純なスクリプト コードを作成します。
function Method2(doc)
{
var xsl = new ActiveXObject("Microsoft.XMLDOM");
xsl.async = false;
xsl.load("hamlet.xsl");
return doc.transformNode(xsl)
}
実行時間は 203 ミリ秒で、7 倍以上高速になっています。これは XSL を使用すべき理由の 1 つであるといえます。さらに、異なるレポートが欲しくなったときには、コードを書き換えるよりも XSL テンプレートを更新する方が簡単です。
問題は、XSL が非常に強力であるということにあります。ぶら下がることができるロープがたくさんある、とでも表現すればいいでしょうか。XSL は、ドキュメント上を任意の順序でウォークするために使用できるリッチな表現言語を持っています。高度な再帰性があり、MSXML パーサーのスクリプト サポートはさらに拡張性を高めています。これらの機能を深く考えずに使用していると、XSL スタイル シートの実行速度が低下してしまいます。以下のセクションでは、注意しなくてはならないいくつかの罠について具体的に説明します。
スクリプティング
XSL スタイル シートの中からのスクリプトの呼び出しは便利であり、優れた拡張性メカニズムです。しかし、例によって落とし穴があります。スクリプト コードは遅いのです。例として、上に示したものの代わりに、次のスタイル シートを書いたとしましょう。
<xsl:template xmlns:xsl="http://www.w3.org/TR/WD-xsl">
<xsl:for-each select="/PLAY/ACT/SCENE/SPEECH[SPEAKER='HAMLET']">
<xsl:eval>this.text</xsl:eval>
<hr/>
</xsl:for-each>
</xsl:template>
これは同じ結果を生成しますが、実行時間は 203 ミリ秒から 266 ミリ秒に伸び、23% も遅くなっています。xsl:eval 文の実行頻度が高くなるほど、パフォーマンスも低下します。実験として、内側の for-each ループの中の xsl:eval を移動してみましょう。
<xsl:for-each select="LINE">
<xsl:eval>this.text</xsl:eval>
</xsl:for-each>
実行時間は 516 ミリ秒で、2 倍に伸びています。要するに、XSL でスクリプト コードを使用するときには、慎重を期す必要があります。
厄介な "//" 演算子
"//" 演算子には注意してください。この小さな演算子は、サブツリー全体をウォークしてマッチを探します。開発者は、フル パスを入力するのが面倒なためか、必要以上にこの演算子を使用する傾向があります (私自身もよく使っています)。たとえば、上の例の select 文を次のように変更してみましょう。
<xsl:for-each select="//SPEECH[SPEAKER='HAMLET']">
選択に要する時間は、203 ミリ秒から 234 ミリ秒に増大します。面倒な作業を嫌ったせいで、15% の負担がかかってしまったことになります。
検索ツリーの刈り込み
検索ツリーを「刈り込む」ことができるのならば、ぜひそうしてください。たとえば、Hamlet.xml に含まれる Bernardo のすべてのセリフを報告しなくてはならないとします。Bernardo のセリフは、いずれも第 1 幕にあります。この事実がわかっている場合には、第 2 幕から第 5 幕までに対する検索を完全にスキップすることができます。次の例は、新しい select 文を示しています。
select="/PLAY/ACT[TITLE='ACT I']/SCENE/SPEECH[SPEAKER='BERNARDO']"
これにより、実行時間は 141 ミリ秒から 125 ミリ秒に減少し、11% の改善が実現されます。
クロススレッディング モデル
以前は、transformNode メソッドと transformNodeToObject メソッドを使用するためには、スタイル シートのスレッディング モデルと変換対象のドキュメントのスレッディング モデルが同じでなくてはなりませんでした。MSXML January 2000 Web Release では、レンタル ドキュメントでフリースレッド スタイル シートを使用したり、その逆を行うことができるようになっています。つまり、レンタル ドキュメントを使って得られるパフォーマンスの向上と、スレッド間でフリースレッド スタイル シートを共有することによって得られるパフォーマンスの向上の両方の恩恵を受けることができます。
結論
Microsoft の XML チームは、過去 3 回のリリースで、MSXML パーサーのパフォーマンスを見事に改善しました。Charlie Heinemann の記事 「Windows 2000 における Microsoft XML パーサー新機能」 の末尾で説明されているように、Windows 2000 リリース用の MSXML のスケーラビリティは大幅に改善されました。MSXML January 2000 Web Release では、スキーマと XSL テンプレートのキャッシングを可能にする新しいインターフェイスが追加されました。また、Web Release には、XSL 変換のメモリ管理を改善し、サーバー上でのスケーラビリティを向上させる重要な変更がいくつか含まれています。
残念ながら、この記事では新機能の紹介をこれ以上行う余裕がありません。読者が大きな関心を抱くようであれば、フォローアップの記事を書くこともあると思います。この記事のパフォーマンスの数値を得るために使った C++ ソース コードと実際のデータは、この記事に付属する xmlperf.exe ファイルに含まれています。
Chris Lovett は、Microsoft XML チームのプログラム マネージャです。