Appendix

id:NyaRuRu:20051021:p1 の補足です.
以下の話のほとんどは『インサイドMicrosoft Windows (上)』に書かれています.より正確で詳細な内容が知りたい方は是非参照してみてください.

インサイド Microsoft Windows 第4版〈上〉 (マイクロソフト公式解説書)

インサイド Microsoft Windows 第4版〈上〉 (マイクロソフト公式解説書)

メモリ周りを見るときの Process Explorer の準備

まず Process Explorer をダウンロードし実行します.本当はカーネル内部変数からも情報を引っ張り出せるようにシンボルサーバの設定をしておくべきなんですがその辺の話は省略*1
次に,プロセスリストの表示項目に少なくとも以下の 4 つの項目が表示されるように設定を変更します.

  • Page Faults
  • Working Set
  • Private Bytes
  • Virtual Size

さらに,メニューから以下のように設定を行います.

  1. View → Show Lower Pane がチェックされていることを確認
  2. View → Lower Pane View → DLLs をチェック

下部ペインに DLL のリストが表示されるようになったら,以下の 3 つの項目が表示されるようにしてください.

  • Base Address
  • Mapped Size
  • Memory Mapped

*1:『インサイドMicrosoft Windows (上)』 1.3.4 カーネルデバッギング

デマンドページング

例えば,VirtualAlloc で 256MB のメモリを確保し,先頭から何か書き込んでいくというプログラムで実験してみましょう.

VirtualAlloc ( NULL, 1024 * 1024 * 256, MEM_COMMIT, PAGE_READWRITE );

各時点での値は,以下のようになります.

Page Faults Working Set [KB] Private Bytes [KB] Virtual Size [KB]
1. 起動直後 304 1,232 292 7,780
2. VirtualAlloc直後 304 1,232 262,692 269,924
3. 128MBまで書き込み 33,105 132,564 262,692 269,924
4. 256MBまで書き込み 65,907 263,900 262,692 269,924
5. ウィンドウ最小化 65,912 328 262,692 269,924

VirtualAlloc の直後に増えているのは "Private Bytes" であって,実際にアクセスするまでは "Working Set" は増えていないというのがポイントですね.これが示しているのは,実際にメモリアクセスが行われるまでは物理メモリの割り当てが遅延されていることです*1.また,id:NyaRuRu:20050607:p1 でも紹介したように,ウィンドウ最小化すると "Working Set" が減少しています.これは,ウィンドウ操作に関するヒューリスティックに基づいた最適化と言えるでしょう.
このように "Working Set" は物理メモリの占有量と密接な関係は確かにあるのですが,メモリのアクセスパターンや他のアプリケーションの物理メモリ使用具合などを元に OS が決定する数字であるため,プログラムのメモリアルゴリズムを推測するのには不向きです.そして Windows 標準のタスクマネージャがプロセスごとの「メモリ使用量」として表示しているのは "Private Bytes" (修正:"Working Set") の値の方なのです.
例えば,/.JFirefox の「メモリ使用量」に関する FAQ としてしばしば紹介されている blog エントリには,こんなことが書いてあります.

オレはもともとの値だった 4MB (4096) で試してみたが、特に問題もないしメモリの使用量もだいたい 50〜60MB前後のところから増えることがなくなった。(一時的に増えたときは、ウインドウを最小化すると一気に解放してくれる)。

これは恐らく "Working Set" の値を見ておられるのだと思いますが,実際これは Firefox のメモリ空間の内どれだけ物理メモリに残すかという OS 側の目標値が減少しただけで,(追記:最小化時には) Firefox はメモリの解放どころか何もしていないと考えられます.
(修正:FireFoxFirefox,thanks to Firefox スレの人)

*1:『インサイドMicrosoft Windows (上)』7.9.1 デマンドページング

ソフトページフォールト

"Working Set" から外されたメモリページは,直ちにディスク上に退避されるわけではありません*1.ページが「変更済みページリスト」或いは「スタンバイページリスト」に留まっている間に再度メモリアクセスが発生した場合は,ページはページフォールトを経てすぐにワーキングセットに復帰します.このようなディスクアクセスを伴わないページフォールトは「ソフトページフォールト」と呼ばれています.
"Working Set" 上のメモリアクセスに比べれば,ソフトページフォールトを伴うメモリアクセスは当然オーバーヘッドを伴います.実際これは簡単なコードで計測可能です.x86 CPU 上の Windows XP はデフォルトで 4KB ページを使用するので,ページフォールトの影響を見るだけであれば 4KB ごとにアクセスすれば十分です.256MB のバッファであれば 256MB / 4KB = 65536 回の DWORD 書き込みで検出可能なはずです.手元の環境 *2 では次のようになりました.

以下にサンプルコードを載せておきました.プロセスワーキングセット解放のために,SetProcessWorkingSetSize API を使用しています.

int main()
{
    const DWORD numByte = 1024 * 1024 * 256;
    DWORD* buf = (DWORD*) VirtualAlloc ( NULL, numByte, MEM_COMMIT, PAGE_READWRITE );

    const DWORD numDWord = numByte / sizeof(DWORD);
    const DWORD skip = 4096 / sizeof(DWORD);

    // 最初のメモリアクセスは,Demand-Zero Page Fault を発生させる
    for( int i = 0; i < numDWord; i+=skip )
    {
        buf[i] = i;
    }

    DWORD tmp = 0;

    // ワーキングセット上のメモリアクセスは Page Fault を発生させない
    for( int i = 0; i < numDWord; i+=skip )
    {
        tmp += buf[i];
    }

    // ワーキングセットからバッファを追い出す
    BOOL ret = SetProcessWorkingSetSize( GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1 );
    
    Sleep( 1000*1 );

    // 変更済みページリストへのアクセスは Soft Page Fault を発生させる
    for( int i = 0; i < numDWord; i+=skip )
    {
        tmp += buf[i];
    }

    return tmp;
}

*1:『インサイドMicrosoft Windows (上)』 7.11.1 ページリストと状態遷移

*2:Windows XP SP2,Intel XEON(Prestonia)@2GHz×2,DDR DPC2100 ECC 1536MB

ハードページフォールト

"Working Set" から外されたメモリページの内容がディスク上に書き出され,元々の物理メモリの内容が消去されてしまうこともあります*1.この後に該当アドレスにメモリアクセスが発生すると,HDD に退避したデータを物理メモリに読み出した上で動作が継続されます.このようなディスクアクセスを伴うページフォールトは「ハードページフォールト」と呼ばれています.ソースコード上はただのメモリアクセスという箇所あっても,非常に長時間処理をブロックされる可能性があることに注意してください.
これが実際どれぐらい影響を及ぼすかは,以下のように確かめられます.上のサンプルの SetProcessWorkingSetSize を呼び出した直後にプログラムを一時停止させ,いわゆる「メモリ最適化系」のツールを実行してみます.今回は「メモリの掃除屋さん」を利用させて頂きました.
http://www6.plala.or.jp/amasoft/soft/index.html
SetProcessWorkingSetSize の呼び出しが終わってバッファのを "Working Set" から追い出したところでプログラムを一時停止し,「メモリの掃除屋さん」の「手動で掃除」を「90%」の設定で実行します.その後プログラムの動作を継続させた結果がこれです.

*1:『インサイドMicrosoft Windows (上)』7.11.1 ページリストと状態遷移, 7.11.2 変更ページライタ

pfmon.exe

「ソフトページフォールト」,「ハードページフォールト」は,OS サポートツールや Platform SDK に含まれる pfmon を使っても観察できます.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/tools/tools/pfmon.asp

メモリマップドファイルとワーキングセット

メモリマップドファイルも "Working Set" を消費します.試しに 256MB のファイルをメモリ空間にマッピングしてみましょう.
まずはファイルの作成からです.@IT で紹介されている『巨大なサイズのファイルを簡単に作る方法』で 256MB のファイルを作成しました.

fsutil file createnew image.bin 268435456

では,このファイルにデータを書き込みながら仮想メモリの使用状況を見ていきましょう.

int main()
{
    Sleep( 1000 * 3 );

    const DWORD numByte = 1024 * 1024 * 256;

    HANDLE file = CreateFileW( L"image.bin", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
        OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL );
    HANDLE mmap = CreateFileMappingW( file, NULL, PAGE_READWRITE | SEC_COMMIT, 0, numByte, NULL );
    DWORD* buf = (DWORD*) MapViewOfFile( mmap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, numByte );

    const DWORD numDWord = numByte / sizeof(DWORD);
    const DWORD skip = 4096 / sizeof(DWORD);

    // Page Fault を発生させる
    for( int i = 0; i < numDWord; i+=skip )
    {
        buf[i] = i;
    }

    UnmapViewOfFile( buf );
    CloseHandle( mmap );
    CloseHandle( file );

    return 0;
}


まずこれはプロセス起動直後のものです."vmtest.exe" というのが今回用意したテストプログラムですね.


次に MapViewOfFile 直後のものです."Virtual Size" が増加して,image.bin がメモリマップされたことが下部ペインに見て取れます.一方,"Private Bytes" はほとんど増加していないことに注意してください.


さらに 128MB 程度アクセスした時点でのメモリ使用量です.デマンドページングによって "Working Set" が必要量割り当てられているのが分かります.


最後の 256MB 全てアクセス完了した時点でのメモリ使用量です.OS によってファイルアクセスのためのバッファとして 256MB 程度の物理メモリが割り当てられたということになります.

あまりにも巨大なファイルをメモリ空間にマッピングする場合には,ワーキングセットの消費量に注意するようにしましょう.メモリアクセスのパターンによっては,一時的に大量な物理メモリが割り当てられる可能性があります.

レスポンスチューニング

さて,以上のように Windows の仮想メモリの働きについて見てきましたが,近年の搭載メモリ量の増加と共に一部パフォーマンスに問題が発生しているような局面も見受けられるようになってきました.
例えば 512MB の "Working Set" を持つアプリケーションのウィンドウを最小化すると,デフォルトでは "Working Set" の縮小が行われますが,ハードウェアスペックによってはこのときの縮小作業自体が軽快な UI 動作を損なうことがあります.さらにこの後アプリケーションのウィンドウサイズを復元した際にはページフォールトが多数発生しますし,ハードページフォールトが引き起こされた場合のレスポンスの悪化は非常に大きなものがあります.
例えば,以下のような点について議論が必要かもしれません.

  • ウィンドウ最小化という UI 操作をトリガーに使うことの是非
  • メモリ上にキャッシュするだけでいつでも高速にデータを再利用できるというプログラマの思いこみ
  • エンドユーザの無知による誤ったクレームの増加

近年扱うデータ量が増加してきたブラウザ,オフィススイート,統合開発環境といったアプリケーションでは特にこの問題が表面化しています.
http://forum.mozilla.gr.jp/?mode=al2&namber=9306&rev=&0&KLOG=62
https://bugs.eclipse.org/bugs/show_bug.cgi?id=85072
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5043070
レスポンスチューニングにはいくつかの方法が考えられますが,SetProcessWorkingSetSize で "Woking Set" 最大値を増加させる場合には注意が必要です.

解説

最小ワーキングセットサイズと最大ワーキングセットサイズの両方に 0xffffffff を指定すると、指定したプロセスのワーキングセットを空にすることができます。
dwMinimumWorkingSetSize と dwMaximumWorkingSetSize のどちらかがプロセスの現在のワーキングセットサイズを超える場合は、指定したプロセスに SE_INC_BASE_PRIORITY_NAME 権が必要です。一般に Administrators グループまたは Power Users グループのユーザーであればこの権限を持っています。 オペレーティングシステムワーキングセットを先着順に割り当てます。たとえば、64MB のメモリを搭載しているシステムで、あるアプリケーションがその最小ワーキングセットとして 40MB を獲得した後、別のアプリケーションが 40MB のメモリを要求すると、オペレーティングシステムは 2 番目のアプリケーションの要求を拒否します。

MSDN の説明に反して,少なくとも Windows 2000 以降の OS では Power Users グループはデフォルトで SE_INC_BASE_PRIORITY_NAME アクセス権 (SeIncreaseBasePriorityPrivilege) を持ちません.VirtualLock で物理メモリ上にロックできるメモリは "Woking Set" 最大値以下に制限されるため,一般的な Power Users グループでの運用では多量のメモリを物理メモリ上に固定することはできないことに注意してください.ただし,最大・最小サイズに 0xffffffff を指定する (x64環境では 0xffffffffffffffff) ことで "Woking Set" を減少させることは,SeIncreaseBasePriorityPrivilege を持たないユーザでも可能です.あくまで増加させることに特権が要求されます.
一方,Microsoft はページフォールト発生パターンの履歴を積極的に活用するという最適化を OS 側に取り込みつつあります.OS やプロセス起動時には大量のページフォールトが発生することが分かっており,起動時間を遅くする要因のひとつと考えられています.Windows XP では,特に影響が大きいハードページフォールトについて,予め必要なファイルを調べておき HDD のシークを減らすように最適化した事前読み込みを行うというメカニズムが導入されました*1
Windows Vista ではさらに改良を加えた SuperFetch と呼ばれる機構が導入される見込みです.PDC で行われた USB メモリを物理メモリの補助に使うというデモをご存じの方もいらっしゃるかもしれませんが,それも SuperFetch の機能のひとつです.

これに対し、Suse Linuxのカーネルを開発するAndrea Arcangeliは、SuperFetchによるパフォーマンス向上効果について懐疑的な見方をしている。

イタリアのイモラに住むArcangeliは、電子メールでのインタビューに答え、「SuperFetchは、キャッシュをすぐに空にする128Mバイトのメモリしかないシステムでは有用かもしれないが、1Gバイトのメモリを搭載したシステムで大きな違いが出るかどうかは疑わしい。また、一見したところでは、システムを複雑にするだけの価値があるようには思えない」と語った。

128MB の環境で Vista を使用するのは無謀かもしれませんが,一方で多くのユーザーが「CPU もメモリも数字は非常に大きくなっているのに,思ったほどは動作が軽快になっていない」と感じているのは確かではないでしょうか.私は現在の Windows のメモリ戦略に若干疑問を持っています.物理メモリに 1GB 以上も空きがあるのに,最小化のたびに数百MB の "Woking Set" 縮小は本当に必要でしょうか? (本当に必要なのはウィンドウ最小化しなくてすむぐらい広いディスプレイ?) こうした疑問に,Vista が何らかの回答を示してくれることを期待しましょう.

*1:『インサイドMicrosoft Windows (上)』7.10 ロジカルプリフェッチャ