Windows で Large Page は「使える」か?

VirtualAlloc で実験していたネタもついでに放出.



Firefox 版 jemalloc のソースを読んでいて,デフォルト chunk size が 1 MB と比較的大きかったので,「それなら Large Page 割り当てても良いんじゃなかろうか?」と調べてみました.もっとも,結論から言えば,Windows 環境の Firefox のメモリアロケータに Large Page を使うのは,全く現実的ではないということが分かっただけでしたが.

Large page support

Windows Server 2003 から,ユーザーモードアプリケーションでも large page が使用できるようになった.Windows Vista でもサポートされる.VirtualAlloc に MEM_LARGE_PAGES フラグを付ける.large page の最小サイズは GetLargePageMinimum で取得できる.

Large Page は,ランダムアクセスが頻発する状況下での TLB ミスヒットを減らすのに効果的と言われています*1.と言われても実際試してみないと実感が沸かないので,今回はソフトウェア環境,ハードウェア環境ともに現状を軽く調べつつ,ついでにベンチマークを取ってみました.

Large Page を使うための OS 環境

Windows のユーザモードアプリケーションで Large Page を使えるようになったのは比較的最近で,カーネルの世代的には Windows Server 2003 以降でサポートされています.Vista では当然サポートされていますし,Windows Server 2003 カーネルが流用されている Windows XP x64 Edition でも使えます.
詳細については,MSDN Library の『Large Page Support』というページにまとまってます*2

Large Page Support

Large-page support enables server applications to establish large-page memory regions, which is particularly useful on 64-bit Windows. Each large-page translation uses a single translation buffer inside the CPU. The size of this buffer is typically three orders of magnitude larger than the native page size; this increases the efficiency of the translation buffer, which can increase performance for frequently accessed memory.

To use large-page support, do the following:

  1. Obtain the SeLockMemoryPrivilege privilege by calling the AdjustTokenPrivileges function. For more information, see Changing Privileges in a Token.
  2. Retrieve the minimum large-page size by calling the GetLargePageMinimum function.
  3. Include the MEM_LARGE_PAGES value when calling the VirtualAlloc function. The size and alignment must be a multiple of the large-page minimum.

The following are restrictions when using large pages:

  1. These memory regions may be difficult to obtain after the system has been running for a long time because the space for each large page must be contiguous, but the memory may have become fragmented. This is an expensive operation; therefore, applications should avoid making repeated large page allocations and allocate them all one time at startup instead.
  2. The memory is always read-write, fully cacheable, and nonpageable. The memory is part of the process private bytes but not part of the working set.
  3. Large-page allocations are not subject to job limits.
  4. WOW64 on Intel Itanium-based systems does not support 32-bit applications that use this feature. The applications should be recompiled as native 64-bit applications.

使用時のポイントをまとめてみます.

  1. 「コントロール パネル\管理ツール\ローカル セキュリティ ポリシー\ローカルポリシー\ユーザー権利の割り当て」にて,「メモリ内のページのロック」特権を対象ユーザに事前に割り当てておく
    • デフォルトでは管理者ユーザすらこの特権を持っていません.Large Page の応用は基本的に特殊なサーバ向けと割り切ることになりそうです.Large Page を必要とする処理をサービスとして動かすことにして,実行ユーザを System ユーザにするというのもひとつの手でしょう.
  2. 以下は実際のコーディング
    1. OpenProcessToken/LookupPrivilegeValue/AdjustTokenPrivileges を使って,SE_LOCK_MEMORY_NAME 特権を獲得する.
      • Explorer から「管理者として起動」してもこの特権は持っていません.
    2. MEM_LARGE_PAGES フラグを付けて VirtualAlloc を呼ぶ
  3. テスト時の注意
    1. そのユーザが特権を持っていて,かつ AdjustTokenPrivileges による特権獲得が成功する必要がある.
      • Vista の場合 UAC に注意しましょう.「管理者として起動」を活用してください.
  4. VirtualAlloc が失敗した場合の戻り値と対策
    1. 「0x00000522 クライアントは要求された特権を保有していません。 」
      • そのユーザが特権を持っていて,かつ AdjustTokenPrivileges による SE_LOCK_MEMORY_NAME 権限獲得が成功している必要があります.
      • Vista の場合 UAC に注意して下さい.「管理者として起動」を活用.今回は Visual C++ 2008 ごと「管理者として起動」しておくと無難です.
    2. 「0x000005aa システム リソースが不足しているため、要求されたサービスを完了できません。」
      • 要求メモリ量にもよりますが,物理メモリの連続領域が不足している可能性が高いです.
      • メモリ割り当てを行うスタートアップアプリケーションやサービスを極力オフにし,OS 起動直後に適度なメモリサイズで試してみてください

私が普段使っているノート PC の Large Page では,1 ページ = 2 MB です*3.MEM_LARGE_PAGES フラグを付けて VirtualAlloc で 10 MB 要請した場合,5 ページが割り当てられることになります.

Windows Vista のメモリ マネージャは、大容量ページの範囲を、以前の Windows リリースより迅速に割り当てます。範囲全体が隣接している必要はなくなりました。したがって、大容量ページを割り当てる試みは、成功する可能性が高くなり、ページ スラッシングが生じる可能性は低くなります。たとえば、アプリケーションが 10 MB の大容量ページを要求する場合、Windows Vista およびそれ以降の Windows リリースは、10 MB の物理的に隣接したメモリを探す代わりに、それぞれが 2 MB の、5 つの大容量ページを割り当てることができます (大容量ページが各ハードウェア プラットフォームで 2 MB の場合)。

2 MB の連続領域 5 つぐらい,普通はどこかにあるだろうという気がしますが,物理メモリの断片化は意外と厄介で,手元の Vista 環境では OS 起動直後でないと確保できませんでした.起動後しばらく放っておくと,合計 10 MB の Large Page ですら 0x000005aa エラーで確保できない有様です.断片化恐るべし.
また,特権に関しても色々とややこしい事情が絡んでいるように思います.
Large Page として確保したメモリは,物理メモリ上に「Lock」されたという扱いで,一般的なワーキングセットからは除外されています.実際,Large Page として確保したメモリは,タスクマネージャ等に表示されるワーキングセットサイズに含まれていません.同じような理由で,Large Page として確保したメモリは,絶対にページファイルへ退避されません.必ず物理メモリ上に存在します.Large Page の確保に SE_LOCK_MEMORY_NAME 権限が必要なのはこのためと考えられます.
もし Large Page を物理メモリ上にロックせず,通常のワーキングセットのように物理メモリから除外できるようにしてしまうと,何が起きるのでしょうか? 恐らく,ハードページフォールト発生時に,連続した物理メモリが確保できる保証が無いのが問題なのでしょう.現在の Windows では Small Page が優勢で,放っておくと Large Page サイズの連続空き領域はどんどん減少していきます.となると物理メモリ上で Large Page 領域を固定しておくしか手は無いのでしょう.

Large Page に対応しているアプリケーション/サービス

サーバ向けアプリケーション/サービスで Large Page をサポートしているものを紹介してみます.もっとも,先ほど述べた物理メモリの断片化の問題があるので,OS 起動直後でないとなかなかうまく行かないかもしれませんが.あと「メモリ内のページのロック」特権と管理者権限も必要になるでしょう.

Sun JVM
Java Support for Large Memory Pages
Microsoft SQL Server
http://blogs.msdn.com/slavao/archive/2005/02/11/371063.aspx:title=
memcached
OpenSolaris 環境での Large Page support

ちゃんと確認していませんが,memcached の Large Page サポートは今のところ Solaris 専用かな?

ベンチマークと結果

Large Page と Small Page それぞれで確保したメモリに,以下のように整数疑似乱数列を書き込んで,STL のソートにかかる時間を比較してみました.

std::tr1::mt19937 mt(static_cast<unsigned int>(d));
for(int i = 0; i < num; ++i)
{
    arr[i] = mt();
}

TLB ミスヒットの影響を見たいので,メモリアクセスの局所性が悪いと予想されるヒープソート (std::make_heap + std::sort_heap) と,さらに比較対象としてクイックソート (std::sort) のそれぞれで測定してみました.ランダムシードは揃えてあります.基本的にページサイズのみの違いです.
環境と結果は以下の通りです.


ランダムにあちこちアクセスするヒープソートに予想通り影響が現れています.64 MB のヒープソートで,Large Page を使うことにより約 6 % の速度向上が見られます.こうやって見てみると,確かに TLB ミスヒットは起きてるんだなーと実感.
クイックソートの方は,メモリアクセスの局所性が良いためか,ヒープソートほどの影響は見られませんでした.

まとめ

現状,Windows での Large Page サポートは色々クセが強く,一般アプリケーションが採用を検討する段階では無いと言えます.とはいえ,Windows Vista と Visual C++ があれば,誰でも Large Page の効果を実測できるようになっていることも分かりました.メモリアクセス律速な処理では,メモリ確保時のパラメータを変えるだけで数 % パフォーマンスが変化しうるというのもなかなか新鮮.memcached のようなサービスで Large Page がサポートされるのも頷ける話です.
とまあ Windows で Large Page は (普通の用途には)「使えない」けど十分「遊べる」というところでしょうか.

Attachment

生データ (Excel 2007 形式)

*1:この他.Large Page 使用時は必要な Page Table Entry の数が減るので省メモリという視点で,『[http://www.ibm.com/developerworks/jp/linux/library/l-mem26/#IDAUC2VG:title=ラージページ - カーネル比較: 2.6カーネルで改善されたメモリ管理]』に解説があります.

*2:なお,MSDN Library で見つけた Large Page 関連の記事は [http://msdn2.microsoft.com/en-us/library/Tags-Cloud.aspx?tag=largepage:title=largepage というタグ] を打つようにしています.現時点でまだ 2 つだけですが,参考になりましたら幸いです.

*3:DEP (Data Execute Prevention) が有効になっているため PAE (Physical Address Extension) も有効化されている.PAE 使用時の x86 CPU の Large Page は 2MB,PAE 無効時の x86 CPU の Large Page は 1 ページ 4MB