プロセス終了時のページイン: 実験

アプリケーション終了時の大量のページインをちょっと実験してみました.

std::vector<T> の場合

#define ZEROCLEAR_IN_DESTRUCTOR
struct DummyData
{
public:
    int m_data;
    DummyData() : m_data(rand())
    {

    }
#ifdef ZEROCLEAR_IN_DESTRUCTOR
    ~DummyData()
    {
        m_data = 0;
    }
#endif
};

class CMyWindow : public CWindowImpl<CMyWindow>
{
public:
    std::vector<DummyData> m_data;
    CMyWindow()
    {
        // 64 MB ほど確保
        m_data.resize( 1024 * 1024 * 16 );
    }
    ......

こんな感じで 64 MB 程度メモリを消費するプログラムを用意して,外部から SetProcessWorkingsetSize + 「メモリの掃除屋さん」で無理矢理ページアウトさせ,その後プロセスを終了させた時にどういう挙動を示すか調べてみました.コンパイラは Visual C++ 2005 のデフォルト設定,実行環境は Windows Vista 32 bit 版,搭載メモリは 2GB です.また,実験用に SuperFetch と ReadyBoost サービスはオフにしてあります.
十分ページアウトさせてからアプリケーションを終了させると

デストラクタを定義しない場合
ページインは無視できる範囲内.
デストラクタを定義してメモリアクセスを行った場合
64 MB 程度のページインが発生.

というわけで,vector の場合,デストラクタで何か作業をしなければ,中身がページアウトしていても問題なさそうでした.

std::list<T> の場合

struct DummyData
{
public:
    int m_data;
    DummyData() : m_data(rand())
    {

    }
};

class CMyWindow : public CWindowImpl<CMyWindow>
{
public:
    std::list<DummyData> m_data;
    CMyWindow()
    {
        m_data.resize( 1024 * 1024 * 16 );
    }
    ......

リンクリストなので vector に比べて要素ごとに余分なメモリオーバーヘッドがあるのと,一要素ごとにメモリを確保するのとで,1600 万要素でもコミットサイズは 400 MB 程度になります.

デストラクタを定義しない場合
プロセス終了時に 20 秒程度のディスクアクセス.

このように,list の場合は,たとえデストラクタが存在しなくてもページインが発生しています.懐かしの「malloc and free」論争ではありませんが,この場合はまさに,メモリを解放するためわざわざページインを起こしているわけで,やはり少し気になる動作と言えます.
ちなみに,無理矢理ページアウトさせているときのパフォーマンスカウンタの変化の様子も記録してみました.

外部からこのプロセスのワーキングセットの縮小を図ったタイミングで,PC 全体の Total Working Set が 400 MB 程度縮小し,代わりに Modified Page List が増加しています.その後「メモリの掃除屋さん」を実行すると,一時的に大量のワーキングセットが消費され,この後 Modified Page List の減少と Standby Cache Normal Priority Byte の増加が続いています.
ここで,各データの意味を簡単に説明.

Total Working Set
PC 全体で物理メモリに「キャッシュ」されているデータのうち,最近アクセスされたとみなされる部分のデータ量
Modified Page List
PC 全体で物理メモリに「キャッシュ」されているデータから Total Working Set に含まれる部分をのぞいた上で,「キャッシュ」の内容の方が新しく,ファイル (またはページファイル) に書き戻す必要があるデータ量
Standby Cache
PC 全体で物理メモリに「キャッシュ」されているデータから Total Working Set に含まれる部分をのぞいた上で,「キャッシュ」の内容と,ファイル (またはページファイル) の内容が同一なデータ量

Modified Page List の減少と,対応する Standby Cache Normal Priority Byte の増加こそが,先ほどの「メモリ内容の書き出し」に相当しています.「メモリの掃除屋さん」実行中の書き出しは急激に行われていますが,「メモリの掃除屋さん」の実行終了後の HDD のアクセスの仕方は,全力というほどではなく,1 MB/sec 程度におちつきます.メモリ圧力が消えたことで,書き出しもゆっくりとしたモードに変化したようです.
さて,この時点ではタスクマネージャからは十分にメモリが空いているように見えます.こうやってパフォーマンスカウンタを見ていないと,定期的に点滅するアクセスランプを見て,「また Vista のバックグラウンドサービスが何か余計な動作をしているのかな?」と勘違いしてしまうかもしれませんね.
なお,Standby Cache に変化しても,ページファイルの「キャッシュ」として機能しているので,他に大きなメモリを確保したり,スタンバイモードに変更したりしながら,がんばって「キャッシュ」を追い出して今回の実験を行っています.

.NET の LinkedList<MyData>

public partial class Form1 : Form
{
    public struct MyData
    {
        public int Data;
    }
    public readonly LinkedList<MyData> dataList;
    public Form1()
    {
        Random rand = new Random ();
        dataList = new LinkedList<MyData>();
        for (int i = 0; i < 1024 * 1024 * 16; ++i)
        {
            MyData data = new MyData();
            data.Data = rand.Next();
            dataList.AddLast(data);
        }

最後に .NET の LinkedList<MyData> の場合です.コミットサイズはほぼ同じ 400 MB になりました..NET の場合,Gen 2 GC が発生すると GC ヒープ中のランダムアクセスが多発し,せっかくワーキングセットから追い出したメモリがワーキングセットに戻ってきやすいので注意が必要です.色々苦労しながら実験してみましたが,結果.

データの大半がページファイルにあると思われる状況でのプロセス終了
ほぼノーコスト

というわけで,CLR の場合,プロセス終了時にも特にページインは発生しなさそうという感じでした.ただ,実験状況がネイティブアプリケーションほど安定しないので,この結果はもうちょっと検証した方が良さそうです.これが真実なら,CLR はアプリケーション終了時と通常の GC 発生時で動作を変えていることになり,調べてみると中々おもしろい部分ではないでしょうか?

Windows VistaWindows XP の違い

さて,先ほどの説明で,「ワーキングセットの縮小を行う」という部分がありましたが,ページアウトの発端はこのワーキングセットの縮小にあるとも言えます.実は,Windows XP では,アプリケーションのウィンドウを最小化したときに OS が「ワーキングセットの縮小を行う」という仕様がありました.Windows XP でウィンドウを頻繁に最小化する使い方をしていると,ページアウトが発生しやすくなりるのはこのためです.
この動作は,Windows Vista で変更され,ウィンドウ最小化によるワーキングセットの縮小は発生しなくなっています.Windows XP 時代に,大量のメモリを使用するアプリケーションが頻繁にページアウトしすぎてあちこちで問題になったため,Vista で動作が変更されたものと思われます.この辺の歴史的経緯については,以下をどうぞ.