low-fragmentation heap

会社で開発しているアプリケーションで、プログラムの[閉じる]ボタンを押しても数秒間固まったままになるものがあった。別に、WaitForXXXObject(s)しているわけでもないし、そんなに重い処理をしている訳でもない。理由がわからないまましばらく放置していたのだが、今日になってやっと原因が判明した。たった一つのdeleteに数百ミリ秒のオーダーで時間が消費されていた。別にデストラクタがあるわけでもないクラスのインスタンスでだ。

いろいろと調べたところ、ヒープの断片化が激しいらしい。かといって、断片化を防ぐコードを書くのもなかなか難しい。いくつかのインスタンスには、無理矢理compact()っていう関数を導入してみて、適宜、メモリの再確保によって断片化を押さえるようにしたものの、全体的には、あんまり改善できず。

仕方がないので、ちょっとだけ調べてみると、Windows XP以降では、low-fragmentation heap (日本語に訳すと、おそらく、低断片化ヒープ)というものが導入されている。まさしくこんな状況のためにあるようなAPIだ。見ると、あのメモリ馬鹿食いで悪名高いFirefoxに適用しようとした人々もいるようだ。

さらに、CRTが利用しているヒープのハンドルを返す_get_heap_handleという関数もVS2005で導入されている。ということで、コードを書いてみた。

#if _MSC_VER >= 1400
  // Enable Low-fragmentation heap
  // NOTE: LFH cannot be activated with debugger attached,
  // it's little bit difficult to know LFH status. If you suspect
  // it were not activated, you should add MessageBox or some
  // other code to show LFH is just enabled.
  typedef BOOL (WINAPI *fp_heap_set_info)(
    HANDLE, HEAP_INFORMATION_CLASS, PVOID, SIZE_T);
  fp_heap_set_info hsi = (fp_heap_set_info)GetProcAddress(
    GetModuleHandle(_T("Kernel32")), "HeapSetInformation");
  if(hsi)
  {
    ULONG flags = 2;
    HANDLE hHeap = (HANDLE)_get_heap_handle();
    if(hsi(hHeap, HeapCompatibilityInformation, &flags, sizeof(flags)))
    {
      // MessageBox(NULL, _T("Wow, LFH is enabled!"), _T("Enabling LFH"), MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);
    }
  }
#endif

一応、VS2003以前のコンパイラでもコンパイルはできるように#if-#endifで囲んで、さらに、HeapSetInformationがkernel32に見つかった場合にのみlow-fragmentation heapを利用するようにする。このコード自体は、エントリポイント(_tmainか_tWinMain)のかなり早めの部分に挿入する方がよさそうだ。API自身の説明にはそんなことは書いていないが、そうしないと、ヒープの仕組みをやりかえるときに、何か処理が入りそう。

と、このコードを挿入するだけで、終了時の処理は、300ミリ秒未満になった。これなら普通に使っていて問題にはならない。
非常にすばらしいのだが、なぜ、デフォルトでONになっていないのかは疑問。あまりにもメモリレイアウトが変わりすぎるから互換性などの面で問題でもあるのだろうか?

あと、HeapSetInformationの説明にもあるとおり、デバッガをアタッチしていると、LFHは有効にならないようなので、コードが正しく動いていることを確認したかったら、MessageBoxなり、printfでチェックするしかないみたい。