コミット済みメモリとは何か。メモリ使用量と何が違うのか。(Windows)

タスクマネージャーに表示される「コミット済み」メモリとは何でしょうか。「メモリ使用量」と何が違うのでしょうか。簡単なプログラムを作って、メモリの使用状況を観察してみます。

動作確認環境

  • Windows 11 Home 24H2
  • Visual Studio Community 2022 (Visual C++)

「コミット済み」メモリとは

タスクマネージャーの[パフォーマンス]タブ内の[メモリ]には、「コミット済み」の文字の下に 2 つの数字が表示されています。「/」の左側が「コミットチャージ」(Commit Charge) で、右側が「コミットリミット」(Commit Limit) です。

「コミットチャージ」とは、プログラムが OS に「私にこれだけのメモリを使わせてください」と要求し、OS が「わかりました。コミット(確約)しましょう」と承諾した、そのサイズの合計です。個々のプログラムが要求したサイズをいわば「仮想メモリ管理台帳」にそのまま記入し、足し算した、帳簿上の数字になります。
各プログラムの要求サイズは「コミットサイズ」と言い、タスクマネージャーの[詳細]タブで[コミットサイズ]を有効にすると確認できます。

「コミットリミット」とは、コミット可能なメモリの上限 = 仮想メモリの最大サイズです。言い換えると、物理メモリのサイズ + ページングファイル(一般的な用語ではスワップファイル)のサイズです。2 つ上の画面キャプチャーは、物理メモリ 8 GB + ページングファイル 4 GB 固定の PC で取得したもので、コミットリミットは 11.8 GB となっています。12 GB きっかりでないのは、その右側に表示されている「ハードウェア予約済み」の 192MB(約 0.2 GB)分が引かれているためです。

OS はコミットリミットに達するまではプログラムからの要求を受け入れますが、リミットを超えるとエラーを返します。確約したはずのメモリを提供できなくなる恐れがあるためです(オーバーコミットの防止)。

コミットチャージはメモリの使用量とは異なる点に注意が必要です。
個々のプログラムは「要求した物理メモリをもらった」と思い込んでいます。しかし OS は、

  • 実際のメモリ割り当ては必要に迫られるまで遅延させよう。状況によっては要求以上のメモリを渡してやってもよいが。
  • 物理メモリを割り当てるかページングファイルに追い出すかは、私が状況に応じて決める。

と考えており、そのように行動します。その行動の結果確定した物理メモリの使用量(ただしスタンバイ状態に遷移したメモリを除く)が、タスクマネージャーの「使用中」のサイズになります。

「メモリ使用量 < コミットサイズ」になる例

「メモリ使用量 < コミットサイズ」になる例、すなわち、たくさんコミットしているのに少ししかメモリを使っていない例について見てみましょう。

以下は、[Enter] キーを押すごとに、

  • 1 GB のヒープメモリを確保
  • ヒープメモリの前半 0.5 GB に書き込み
  • ヒープメモリの解放

を行うプログラム MemAllocTest.c です。

#include <stdio.h>
#include <windows.h>

int main()
{
    const int MALLOC_SIZE = 1024L * 1024 * 1024; // 1 GB

    // プログラム起動
    printf("プログラム起動。[Enter] キーで継続。\n");
    getchar();

    // メモリ確保
    char *p = (char *)malloc(MALLOC_SIZE);
    if (p == NULL)
    {
        printf("メモリ確保失敗。\n");
        return 1; 
    }
    printf("メモリ確保成功。p=%p。[Enter] キーで継続。\n", p);
    getchar();

    // 前半に書き込み
    for (int i = 0; i < MALLOC_SIZE / 2; i++)
    {
        *(p + i) = i & 0xFF;  // 特に意味はない
    }
    printf("前半に書き込み成功。[Enter] キーで継続。\n");
    getchar();

    // メモリ解放
    free(p);
    printf("メモリ解放成功。[Enter] キーで終了。\n");
    getchar();

    return 0;
}

Visual C++ x86 版でビルドして、プログラムを起動します。

C:\tmp> cl MemAllocTest.c
......
C:\tmp> MemAllocTest.exe
プログラム起動。[Enter] キーで継続。

起動直後のメモリ使用量は 4.7 GB、コミットチャージは 5.7 GB でした。

[Enter] キーを押して 1 GB のメモリを要求すると、コミットチャージが 1 GB 増えました。要求が受け入れられた格好です。しかし、メモリ使用量に変化は見られません。OS がメモリの割り当てを遅延させているためです。

C:\tmp> MemAllocTest.exe
プログラム起動。[Enter] キーで継続。
メモリ確保成功。p=00A81020。[Enter] キーで継続。

細かく見ると、ヒープの先頭アドレスを含むページ(4 KB)には対応する物理メモリが存在するが、後続のページには存在しないという状態でした。参考までに、WinDbg のカーネルデバッグ機能で物理メモリの有無を調べた様子を示しておきます。

0: kd> !process 0 8 memalloctest.exe  ★ MemAllocTest.exe の情報を表示
PROCESS ffff9581535bc0c0
    SessionId: none  Cid: 20d0    Peb: 00568000  ParentCid: 0550
    DirBase: 193e2c002  ObjectTable: ffffa708cb625ac0  HandleCount:  60.
    Image: MemAllocTest.exe

0: kd> .process /i ffff9581535bc0c0  ★ 侵入的デバッグの対象プロセス指定
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.

0: kd> g  ★ 侵入的デバッグ実行
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff803`772b6330 cc              int     3

5: kd> !pte A81020  ★ 先頭アドレスには物理メモリあり (pfn 1925b4)
                                           VA 0000000000a81020
PXE at FFFFDC6E371B8000    PPE at FFFFDC6E37000000    PDE at FFFFDC6E00000028    PTE at FFFFDC0000005408
contains 8A00000199A35867  contains 0A00000105036867  contains 0A00000239737867  contains 81000001925B4847
pfn 199a35    ---DA--UW-V  pfn 105036    ---DA--UWEV  pfn 239737    ---DA--UWEV  pfn 1925b4    ---D---UW-V

5: kd> !db 1925b4020 L10  ★ その内容((1925b4 * 1000) + (A81020 & FFF))
#1925b4020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................

5: kd> !pte A81020+1000  ★ 1 ページ分後ろのアドレスには物理メモリなし (not valid)
                                           VA 0000000000a82020
PXE at FFFFDC6E371B8000    PPE at FFFFDC6E37000000    PDE at FFFFDC6E00000028    PTE at FFFFDC0000005410
contains 8A00000199A35867  contains 0A00000105036867  contains 0A00000239737867  contains 0000000000000000
pfn 199a35    ---DA--UW-V  pfn 105036    ---DA--UWEV  pfn 239737    ---DA--UWEV  not valid

さて、[Enter] キーを押してヒープメモリの前半 0.5 GB に書き込みを行うと、OS があわてて (?) 本プロセスにメモリを割り当て、メモリ使用量が 0.5 GB 増えました。コミットチャージは変わりません。

C:\tmp> MemAllocTest.exe
プログラム起動。[Enter] キーで継続。
メモリ確保成功。p=00A81020。[Enter] キーで継続。
前半に書き込み成功。[Enter] キーで継続。

最後に [Enter] キーを押してヒープメモリを解放すると、メモリ使用量が 0.5 GB 減少し、コミットチャージが 1 GB 減少し、プログラム起動直後のサイズに戻りました。

C:\tmp> MemAllocTest.exe
プログラム起動。[Enter] キーで継続。
メモリ確保成功。p=00A81020。[Enter] キーで継続。
前半に書き込み成功。[Enter] キーで継続。
メモリ解放成功。[Enter] キーで終了。

このように、プログラムが OS からメモリの利用権を得た(コミットされた)にも関わらず、結局使い切らずにいると、「メモリ使用量 < コミットサイズ」になります。

「メモリ使用量 > コミットサイズ」になる例

逆に、「メモリ使用量 > コミットサイズ」になる例、すなわち、少ししかコミットしていないのに多くのメモリが使われる例について見てみましょう。

ソース全体は省略しますが、以下のような、とても長いコードを含むプログラム MegaLogic.c を作成します。

#include <stdio.h>
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    <とても長いコード(関数呼び出しを含む)>

    MessageBox(0, "end", "MegaLogic", MB_OK);
    return 0;
}

Visual C++ x86 版でビルドした結果、.exe ファイルのサイズは約 0.5 GB になりました。

C:\tmp> powershell "ls megalogic.exe | fl name, length"

Name   : megalogic.exe
Length : 507526656  ★ 約 0.5 GB

.exe ファイルはコード(.text セクション)の塊であり、データはほとんど含んでいません。

C:\tmp> dumpbin /headers megalogic.exe
......
  Summary

        2000 .data
        1000 .fptable
        7000 .rdata
        1000 .reloc
    1E3FC000 .text  ★ コードのサイズは 0x1E3FC000 = 507494400 = 約 0.5 GB

プログラム起動前のメモリ使用量は 4.7 GB、コミットチャージは 5.7 GB でした。

プログラムを実行します。

C:\tmp> MegaLogic.exe

プログラムからの要求であるコミットチャージに変化は見られないのに、メモリ使用量は 0.5 GB 増加しました。

[詳細] タブで確認すると、本プロセスのコミットサイズはゼロではなく 2644 KB(約 2.5 MB、0.0025 GB)であることが分かりますが、わずかな量です。なぜメモリ使用量とこれほどの差があるのでしょうか。

プログラムを実行するには、当該ファイルに含まれるコードをメモリに配置する必要があります。しかし、コード全体を一度に・即座にメモリに配置する必要はありません。当面の実行に必要なコードのみを実行ファイルからメモリに読み込み、必要なくなったらメモリから破棄する、という処理を繰り返せば少ないメモリでプログラムの実行が可能だからです。その少ないメモリとしてプログラムが(というかこの場合は OS が)要求したメモリが、0.0025 GB の控えめなコミットサイズになります。

とはいえ今は、そこまで節約しなくてもメモリは十分に余っています。そこで OS は富豪的にコード全体をメモリにロードし、その結果、メモリを 0.5 GB 消費したというわけです。

このように、OS の判断で、プロセスにコミットサイズ以上のメモリが割り当てられることがあります。

ページングファイルを「なし」にすると、物理メモリが無駄になる?

ページングファイルを「なし」に設定すると、「メモリ使用量の上限」と「コミットチャージの上限」は同じ「物理メモリのサイズ」になります。

この状態で「メモリ使用量 < コミットサイズ」になるプログラム、すなわち、たくさんコミットするのに少ししかメモリを使わないプログラムを多数実行すると、メモリ使用量が限界に達する前にコミットチャージが限界に達し、物理メモリが余っているのにメモリ不足になるという一見不思議な事象が発生します。

試してみましょう。
以下は、物理メモリ 8 GB(ただし利用可能なのはハードウェア予約済み分を除いた 7.8 GB)、ページングファイル「なし」の PC の、ある一時点でのタスクマネージャーです。

ここで、最初に作った MemAllocTest.exe を起動し、[Enter] キーを 1 回押します。すると、コミットチャージが 1 GB 増え、残り 0.5 GB になります。一方、メモリ使用量は増えず、残りは 2.1 GB のままです(8 – 5.7 – 0.2 = 2.1)。

C:\tmp> MemAllocTest.exe
プログラム起動。[Enter] キーで継続。
メモリ確保成功。p=012A4020。[Enter] キーで継続。

この状態で MemAllocTest.exe プロセスをもうひとつ起動し、[Enter] キーを 1 回押すと、1 GB のコミットができず、プログラムが終了します。物理メモリが 2.1 GB 余っているにもかかわらずです。

C:\tmp> MemAllocTest.exe
プログラム起動。[Enter] キーで継続。
メモリ確保失敗。

利用するプログラムの特性を考えずにページングファイルを「なし」(あるいは小さなサイズ)に設定すると、物理メモリが余っているにも関わらずコミットリミット超過に起因するメモリ不足が発生し、もっとメモリを買わなくてはという誤った結論に達することがあるので注意が必要です。

おわりに

コミット済みメモリとメモリ使用量の違いについて見てきました。ポイントをまとめます。

  • コミットサイズとは、プログラムが OS に要求し、OS からコミットされた(割り当てが確約された)帳簿上のメモリサイズであり、単なる数字です。物理メモリやページングファイルの使用量ではありません。
  • 実際のメモリ使用量は状況に応じて OS が決定し、コミットサイズより大きくなったり小さくなったりします。物理メモリを割り当てるかページングファイルに追い出すかも OS が決定します。
  • ページングファイルを「なし」(あるいは小さなサイズ)に設定すると、物理メモリに余裕があるにも関わらずコミットリミットに起因するメモリ不足が発生する恐れが高くなるため注意が必要です。

参考文献

[1] 栗木「タスク マネージャーの見方(Memory)」Microsoft Japan Windows Technology Support Blog, 2022
https://jpwinsup.github.io/blog/2021/05/10/Performance/SystemResource/TaskManagerMemory/

[2] Pavel Yosifovich, Alex Ionescu, Mark E. Russinovich, David A. Solomon 著, 山内 和朗 訳「インサイド Windows 第 7 版 上」 Microsoft, 日経 BP 社, 2018