前回、C言語でのリスト構造を説明しました。その中で、新しいリストを作る度にメモリの確保(malloc)と、リストを削除する度にメモリの開放(free)をしていたのを覚えていますでしょうか。この malloc と free は軽い処理ではありません。また、高頻度で確保と開放を繰り返すと、メモリの断片化を引き起こす可能性があり、最悪の場合はメモリがあるのにメモリの取得に失敗します。今回はこのメモリの取得と開放の頻度を減らす方法です。
--
いきなりここに飛んで来ちゃった人は、よろしければ下記からご覧ください。


  • メモリを取得する
解説は前回のリスト構造プログラムの改造で行いますので、まずは前回の講座内容をよく理解しておいてください。その中で、AddStar() という関数の冒頭で

SStar* star = (SStar*)malloc(sizeof(SStar));
このような記述があります。ここが毎回、SStar構造体を格納するメモリを確保している処理です。このメモリの取得を減らすためには、ある程度まとめてメモリの確保をして、そこから自分でメモリの使い方を管理するのが良いでしょう。

使っているメモリは g_pStar のリストで管理されていますから、使っていないメモリ管理用に、もう一本リストを用意します。

SStar* g_pEmpty = NULL;
そして、星を一つ追加する際は、この g_pEmpty から1つ分の SStar 構造体メモリをもらうようにします。

SStar* star = (SStar*)g_pEmpty; // 空きリストの先頭を使用する if (!star) return false; // 本当は不要 g_pEmpty = g_pEmpty->pNext; // 空きリストを詰める
空きリストを詰める g_pEmpty = g_pEmpty->pNext の処理ですが、g_pEmpty が NULL の可能性があると表示されて警告となります。本当はその前処理を入れるので、g_pEmpty が NULL はあり得ないのですが、静的解析ではない簡易チェックなので、この警告がとまりません。そのため仕方がなく NULL チェックを入れています。速度的にも無駄だと思うのであれば、ソースの何処かに

#pragma warning( disable : 6011 ) #pragma warning( disable : 28182 )
この記述を追加してください。これで警告は消えます。さて、g_pEmpty から空きメモリを受け取るのは良いのですが、そもそもこのリストは最初は NULL になっています。だから、VS2023 も警告を出していたので、これを事前にチェックする必要があります。そこで空きリストの先頭を取得する直前に、空きリストが存在するかどうかを判定して、存在しなければ、まとめてメモリを読み出す処理を呼び出すようにします。

if (!g_pEmpty && !GetMemory()) return false;
この判定式では、空きメモリリストである g_pEmpty が NULL なら、GetMemory() というメモリ確保関数を呼び出して、その結果が失敗(false)なら終了させています。この判定式があるから、g_pEmpty は必ず存在するという保証になります。

※ まだまだ1TBは高いがそれでも以前よりは安くなってる。これだけの容量があればSwitchにかなりゲームを入れられるだろう。

  • まとまったメモリの取得と管理
GetMemory() 関数では、まとめてメモリを取得しています。どれだけまとめて取得すればよいのかは、それぞれのプロジェクトの都合で変わると思います。今回は 128 とします。この数は要調整項目となりますので、定数定義して後から簡単に個数を変更できるようにしておきましょう。

// まとめて取得する数 const int c_nBlock = 128;
さて、malloc でまとめてメモリの取得をするのは良いのですが、これ、最終最後には開放する前提で取得しなければならないので、取得時のポインタアドレスは、別途保存しておかねばなりません。今回は最大10ブロックまで配列でアドレスを管理することにします。グローバル配列として、以下を用意します。

void* g_pMalloc[10] = { 0 };
{ 0 } と初期化子を与えることによって、この配列は全て 0(NULL)でクリアされます。この配列の値が NULL ならまだ取得していないブロックがあるということになります。まとめて取得する数が 128 でしたから、今回は星の数は最大で 1280個までは表示可能となります。表示範囲が 80×25 = 2000 なので、画面全体の半分以上です。もっとたくさん出したい時は c_nBlock の 128 を 256 に変更するなり、g_pMalloc[10] の 10 を 100 に変更するなりで良いと思います。

まず、この配列で NULL になっている場所を探します。

// 動的確保テーブルの空きを探す int idx = -1; for (int i = 0; i < _countof(g_pMalloc); ++i) { if (g_pMalloc[i]) continue; idx = i; break; } if (idx < 0) return false; // 空きがないので終了
これで、idx 変数に空き配列の添字番号が格納されます。その位置の配列に、メモリを確保したポインタアドレスを格納します。

// メモリを動的にまとめて確保する void* ptr = g_pMalloc[idx] = malloc(sizeof(SStar) * c_nBlock); if (!ptr) return false;
SStar 構造体のサイズに、まとめて確保するサイズ c_nBlock を掛けたサイズで malloc を呼び出しています。この時、取得に失敗していればそのまま終了します。これでまとめてメモリは確保できましたが、このままではとても使いにくいので、空きエリアを SStar* のリストで繋げてしまいます。

// 空きリストを繋げる SStar* star = (SStar*)ptr; for (int i = 0; i < c_nBlock - 1; ++i) { star->pNext = star + 1; star = star->pNext; } star->pNext = g_pEmpty; g_pEmpty = (SStar*)ptr; return true;
取得したメモリの先頭を最初の SStar 構造体の空きエリアと見なして、star ポインタとします。この star の次のリスト pNext は、まとめてメモリを取得した次のアドレスなので star + 1 を代入しています。そして、次の star を pNext のアドレスにすることで、次々と空きエリアをリストとして接続していきます。最終最後は次のアドレスは存在しないので、ループ回数は c_nBlock - 1 となっている点にご注意ください。

リストの接続ループを抜けた時は star は最後の空きエリアを示していますから、その最後の次のアドレスに、現在の空きポインタを代入して、全ての空きリストを接続します。そして、一番最初に取得したポインタを、新たな空きエリアの先頭ポインタとして g_pEmpty に格納しています。
※ HDDはこのメーカー一択。なかなか壊れません。

  • メモリを未使用にする
星の表示が消える時は、表示リストから該当の情報を破棄することになります。そのポインタは動的確保したアドレスではなく、g_pEmpty 空きリストから受け取ったアドレスなので、使い終わったのであれば、また空きリストに戻すことになります。

前回のリストでは、最終最後に free(ptr); としていますが、ここを変更します。

// 空きリストに未使用データを追加 ptr->pNext = g_pEmpty; g_pEmpty = ptr;
空きエリアの先頭に戻すだけであれば、リストの繋ぎ変えはこんなに簡単ですね。また、簡単だからこそ、処理が高速になっているのです。


  • 確保したまとまったメモリを開放する
確保したメモリは g_pMalloc 配列に格納されていますので、この配列に格納されたポインタアドレスが NULL 以外なら、そのポインタを free するという動作を繰り返します。また、最後に空きリストと使用リストも初期化します。

void FreeMemory(void) { for (int i = 0; i < _countof(g_pMalloc) && g_pMalloc[i]; ++i) { free(g_pMalloc[i]); g_pMalloc[i] = NULL; } g_pEmpty = g_pStar = NULL; }
これで、メモリの自前管理は出来ました。メモリの確保は自動で行われますので、使う分には、ほぼ自前管理を意識する必要はなくなります。また、メインループも今までと殆ど変わりません。違う点と言えば、プログラムの終了時に一度だけ FreeMemory() を呼び出すことだけです。

int main(void) { const int max = 200; // 最初に全て星を配置する srand((unsigned)time(NULL)); for (int i = 0; i < max; ++i) { AddStar(); } // [Esc]が押されるまで星の削除と追加を繰り返す while (true) { if (_kbhit() && _getch() == 0x1b) break; RemoveStar(); AddStar(); Sleep(20); } // 登録されている全てのメモリを削除する FreeMemory(); printf(ATBCLEAR); printf(SCRCLEAR); printf(LOCATE, 1, 1); return EXIT_SUCCESS; }
少し手間がかかりましたが、これで少なくともゲーム中にメモリの断片化が発生したり、メモリ不足で停止するリストは避けられるようになります。また、動的確保なので使い終わったら FreeMemory() とすることで完全にメモリを明け渡すことが出来るようになります。プロの現場では当たり前に使われているテクニックなので、是非この自前管理法を理解していただければと思います。

なお、g_pMalloc 配列をリストにして、さらに究極的にメモリをケチる事もできますが、100 個用意したところで、64bit マシンでも 800バイト程度です。あまりここを頑張る必要性は感じないですね…。

今回の解説プロジェクトは下記からダウンロードできます。c_nBlock が 128 だと GetMemory() は 2回呼び出されますが、256 にすると最初の一度しか呼ばれません。適当に数値を弄って動作の違いを見てもらえると嬉しいです。

2023.09.16
※ なんでここまで値段が落ちたのって疑問に思うぐらい安い。私が現在使っているのもコレ。高速安定安価で他の選択肢が考えられない。