MASATOの開発日記


前の開発日記 次の開発日記 一覧

2004/03/07

ソフトウェアのパフォーマンスを向上させるための手順は、次の通りです。最初に測定。ソフトウェアの様々な処理の時間を計測します。 次に解析。測定結果を用いてボトルネックがどこにあるのかを明確にします。 最後に対策。ボトルネックをなんらかの方法(アルゴリズム改善、最適化、etc)で解消します。

肝心なのはこの手順通りに行うと言うことです。最初にアルゴリズム改善や最適化を行ってはなりません。 ついつい改善しやすそうなところを改善してしまうというのは良く分かりますが、改善を行うのはそこを改善すれば効果があると解明してからです。

なんてえらそーに言ってみました。

MSXMLのパフォーマンス問題

環境Visual C++.NET 2003
言語C++
使用ライブラリMSXML 3.0

今回は、MSXML3.0が、ある状況でパフォーマンスが劇的に悪化するため、それを回避するにはどうしたら良いか、というお話です。

結論から言うと、ある状況というのは、一つの要素が多数(数千個)の子要素を持っている時に、 夫々の子要素に順番にアクセスした場合であり、 回避するためには、多数の子要素を持たないようにする必要があります。 孫要素の数はパフォーマンスに大した影響がないようなので、うまい具合に階層化すればなんとかなります。

例えば、以下のようなXMLファイルのItem要素全部にアクセスする場合、非常に時間がかかります。

<Root>
  <Item>適当な文字列1</Item>
  <Item>適当な文字列2</Item>
  ...Item要素が数千個続く
  <Item>適当な文字列N</Item>
</Root>

しかし、以下のようなXMLファイルは、Item要素の数が同じであっても、 Item要素全部にアクセスする場合はそれほど時間がかかりません。

<Root>
  <Group>
    <Item>適当な文字列1</Item>
    <Item>適当な文字列2</Item>
    ...Item要素が数十個続く
    <Item>適当な文字列M</Item>
  </Group>
  <Group>
    <Item>適当な文字列M+1</Item>
    <Item>適当な文字列M+2</Item>
    ...Item要素が数十個続く
    <Item>適当な文字列2M</Item>
  </Group>
  ...こんな感じでGroup要素が続く
  <Group>
    ...Item要素が数十個続く
    <Item>適当な文字列N</Item>
  </Group>
</Root>

Item要素全部にアクセスした場合の時間は、Item要素の数にもよりますが、後者の方が1/10の時間で済むことも珍しくありません。 よって、XMLファイルの構造を後者のようにすればMSXML3.0のパフォーマンス問題は回避できます、ということです。

パフォーマンス問題を確認し、回避策の効果を確認できるコードは以下の通りです。

#include <stdio.h>
#include <tchar.h>

#import "msxml3.dll" named_guids

void MakeFlatXml(int n)
{
  MSXML2::IXMLDOMDocument2Ptr pDoc;
  pDoc.CreateInstance(MSXML2::CLSID_DOMDocument);
  MSXML2::IXMLDOMElementPtr Root = pDoc->appendChild(pDoc->createElement(L"Root"));
  for (int i = 0; i < n; ++i) {
    MSXML2::IXMLDOMElementPtr pItem = Root->appendChild(pDoc->createElement(L"Item"));
    pItem->appendChild(pDoc->createTextNode(L"ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
  }
  pDoc->save(L"test.xml");
}

void MakeStructureizedXmlSub(MSXML2::IXMLDOMElementPtr pParent, int n, int num)
{
  MSXML2::IXMLDOMDocument2Ptr pDoc = pParent->GetownerDocument();
  if (n <= num) {
    for (int i = 0; i < n; ++i) {
      MSXML2::IXMLDOMElementPtr pItem = pParent->appendChild(pDoc->createElement(L"Item"));
      pItem->appendChild(pDoc->createTextNode(L"ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
    }
  }else {
    for (int i = 0; i < n; i += n / num) {
      MSXML2::IXMLDOMElementPtr pGroup = pParent->appendChild(pDoc->createElement(L"Group"));
      int m = min(n - i, n / num);
      MakeStructureizedXmlSub(pGroup, m, num);
    }
  }
}

void MakeStructureizedXml(int n, int num)
{
  MSXML2::IXMLDOMDocument2Ptr pDoc;
  pDoc.CreateInstance(MSXML2::CLSID_DOMDocument);
  MSXML2::IXMLDOMElementPtr Root = pDoc->appendChild(pDoc->createElement(L"Root"));
  MakeStructureizedXmlSub(Root, n, num);
  pDoc->save(L"test.xml");
}

void TestPerformance(void)
{
  LARGE_INTEGER Freq, Time1, Time2, Time3;
  ::QueryPerformanceFrequency(&Freq);
  ::QueryPerformanceCounter(&Time1);
  MSXML2::IXMLDOMDocument2Ptr pDoc;
  pDoc.CreateInstance(MSXML2::CLSID_DOMDocument);
    pDoc->put_async(VARIANT_FALSE);
  if (!pDoc->load(variant_t(L"test.xml"))) {
    return;
  }
  ::QueryPerformanceCounter(&Time2);
  MSXML2::IXMLDOMNodeListPtr pList = pDoc->selectNodes("/Root//Item");
  long l = pList->Getlength();
  for (long i = 0; i < l; ++i) {
    MSXML2::IXMLDOMNodePtr pItem = pList->Getitem(i);
    pItem->Gettext();
  }
  ::QueryPerformanceCounter(&Time3);
  printf("Method3:ItemScore Count: %ld  Load %dms Parse %dms\n", l,
    static_cast<int>((Time2.QuadPart - Time1.QuadPart) / (Freq.QuadPart / 1000)),
    static_cast<int>((Time3.QuadPart - Time2.QuadPart) / (Freq.QuadPart / 1000)));
}

int _tmain(int argc, _TCHAR* argv[])
{
  ::CoInitialize(NULL);

  printf("Flat\n");
  MakeFlatXml(1024);
  TestPerformance();
  MakeFlatXml(8192);
  TestPerformance();
  MakeFlatXml(65536);
  TestPerformance();

  for (int n = 2; n <= 1024; n *= 2) {
    printf("N=%d\n", n);
    MakeStructureizedXml(1024, n);
    TestPerformance();
    MakeStructureizedXml(8192, n);
    TestPerformance();
    MakeStructureizedXml(65536, n);
    TestPerformance();
  }
  ::CoUninitialize();
}

上記プログラムで測定した時間を表にしてみました。 測定環境は、Pentium4 3GHz、メモリ1GB、WindowsXP Professional です。
Flatは、全てのItem要素をRoot要素の直下に配置したXMLファイルです。 N=2〜1024は、一つの要素の直下にN個よりも多くの要素が配置されないように階層化したXMLファイルです。
Load時間は、XMLファイルの読み込みにかかった時間で、 Parse時間は、XMLファイルのItem要素全てにアクセスするのにかかった時間です。

Flat N=2 N=4 N=8 N=16 N=32 N=64 N=128 N=256 N=512 N=1024
Item数=1024 Load時間(ms) 5 8 5 6 5 4 4 5 5 5 4
Item数=1024 Parse時間(ms) 40 9 6 6 5 6 6 7 10 16 27
Item数=8192 Load時間(ms) 32 64 51 47 44 36 43 33 33 33 35
Item数=8192 Parse時間(ms) 1537 90 71 54 54 55 70 75 93 138 229
Item数=65536 Load時間(ms) 252 555 368 387 290 358 278 309 259 260 261
Item数=65536 Parse時間(ms) 102521 814 592 474 463 550 585 766 1109 1295 1944

多少は誤差がありますので注意して下さい。
この表を元に考察してみます。

階層化した場合としない場合ではオーダーが違うような気がしますね。 ここまで劇的だと、エンドユーザにまではっきりと分かります。
しかし100倍速くなると言うことは、もともと100倍遅かったと言うことです。がんばれMicrosoft・・・

(2004/03/15) ::CoUninitializeを追加しました。

前の開発日記 次の開発日記 一覧