プログラムのメモリ使用量とMapファイル
コンシューマ機だとメモリ1GB以上が当たり前になりつつある現代、メモリの使用量を減らすときは、プログラムで使用するワークメモリやテクスチャ・モデル・モーションといったアセットデータを削減することがメインになります。
そういう時代なので忘れがちなのですがプログラム自体にもデータサイズが存在し、節約すればメモリの空き容量も増えます。
今日はそんなプログラムのメモリ使用量のお話です。
プログラム自体もメモリを使う
C++で書いたプログラムはアセンブラにコンパイラされ実行ファイルとなります。その実行ファイルを起動する際、OSはそのプログラムの実行に必要なメモリを確保し割り当てます。
この際に確保される容量がプログラム自体のメモリ使用量となります。このメモリ使用量は大きなプログラムであるほど使用量も大きくなります。
プログラムの規模にもよりますが、メモリ使用量は雑にプログラムを組んでいくと1つのゲームアプリで20MBぐらい使用することもあります。
ただし、冒頭にも書きましたがメモリサイズが1GB以上が主流である現行機では、プログラム自体のメモリ使用量がメモリ全体に占める割合はとても小さくなっています。そのためあまり気にしなくてもいいといえばそうかもしれません。
とはいえ、メモリが100MBもないようなハードでは割と気にしないといけません。もしそういうハードの開発をするときは思い出してあげてください。
Mapファイルで内訳を調べる
プログラム自体のメモリ使用量は、Mapファイルを見るとだいたい分かるようになっています。
Mapファイルとはコンパイラが中間コードをリンク処理するする際に出力されるファイルで、リンカにコマンドラインオプションで指定すると出力されるようになります。
Mapファイル出力機能は VC++ や G++ といったメジャーなコンパイラにはもちろんありますし、一般公開されていない OS 専用のコンパイラにもだいたいついています。
Mapファイルは単純なテキストファイルのため、内容はテキストエディタで閲覧することができます。
例えば、VC2015 ですとこのようなMapファイルが出力されます。(長いので先頭部分のみ貼り付けます)
CppTest
Timestamp is 596cb7f0 (Mon Jul 17 22:13:20 2017)
Preferred load address is 00400000
Start Length Name Class
0001:00000000 00010000H .textbss DATA
0002:00000000 00004859H .text$mn CODE
0003:00000000 00000104H .CRT$XCA DATA
0003:00000104 00000104H .CRT$XCAA DATA
(略)
主要なセクション
Mapファイルの形式はコンパイラによって異なります。そのため「Mapファイルはあっても読み方が分からない!」なんてことは結構あります。
でもご安心を。Mapファイルには、だいたい『セクション』というものが記載されている、もしくはそれを匂わす表記がしてあります。この『セクション』をおさえておけばだいたい読めます。
『セクション』とはメモリの使用用途の分け方のことです。
主なセクションは以下の通りです。
- .text : プログラムの命令コード
- 命令数が増えれば増えるほどサイズも増える。
- 起動時に実行ファイルからメモリ上に複製される。
- .bss : ゼロ初期化されるグローバル変数領域(Block Started by Symbol)
- ゼロ初期化な static 変数や無名名前空間変数が増えるほどサイズも増える。
- 起動時にこのサイズ分のメモリを確保する。(内容はゼロなため実行ファイルからコピーされるわけではない)
- CPUによっては .sbss(Small BSS)セクションというのもあり、小さいデータサイズのものは専用の領域に割り当てられることもある。
- .rodata : 読み取り専用データ(Read Only DATA)
- 文字列リテラルや配列の初期化データなど変更しない定数なデータ。
- 起動時に実行ファイルからメモリ上に複製される。
- .data : 読み書きデータ
- .bss の非ゼロ初期化版。
- 起動時に実行ファイルからメモリ上に複製される。
ちなみに各セクションの名前は、コンパイラによって微妙に変わることがあります。正確な情報を知りたい方はコンパイラのドキュメントを確認してください。
プログラムのメモリ使用量削減方法
Mapファイルを見て「これはメモリを使いすぎだ!」となった場合、何らかの手を打ってメモリ使用量を削減することになります。
各セクション毎にメモリ使用量の代表的な削減方法を紹介します。
(.text)最適化オプションをコードサイズ優先にする
例えば、インライン展開をすると実行速度が速くなりますがそのぶんコードサイズが大きくなりがちです。実行速度はそこまで必要ない場合はインライン展開を切ることでコードサイズを減らすことができます。
このインライン展開のように、最適化によってコードサイズが増えるような最適化が存在します。
たいがいのコンパイラには「実行速度優先最適化」と「コードサイズ優先最適化」が用意されていて、後者を選ぶとコードサイズが著しく増えるようなインライン展開が抑制されます。コードサイズを減らす場合は最適化オプションの変更を検討してみましょう。
注釈:コンパイラやコードによっては、インライン展開が有効なときのほうがコードサイズが小さくなることがあります。一概にどっちと言えないようです。
(.text)スクリプトコードに差し替える
そのコードが C++ である必要がない場合、スクリプト化することを検討しましょう。そうするとそのコードが必要なときのみメモリを使用することになるため、(状況にもよりますが)スクリプトのメモリ使用量を考慮したとしてもメモリの総使用量が削減されるはずです。
(.rodata)外部データに抽出する
長い文字列や配列の要素などはプログラムに直接埋め込むのではなく外部データ(ファイル)に逃がして、必要なときにロードして使用するようにしましょう。
(.bss&.data)static変数&無名名前空間変数を使わない
.bss と .data は実行ファイルを起動してから終了するまで、たとえコードを使わない場面でもずっとメモリを使用し続けます。ですので static 変数、および無名名前空間の変数はどうしても必要な場合をのぞいて使わないようにしましょう。
よくこの領域に現れるのが Singleton パターンを使ってクラスを static 変数化してしまったものです。どうしてもシングルトンを使いたい場合はオブジェクトを static 変数化するのではなく、オブジェクトのポインタのみ static 変数化するようにし、常時消費するメモリの使用量を最低限にするようにしましょう。
参考までに、シングルトンを使いたい状況で .bss .data セクションを節約するため、筆者はこんな感じのコードを書きます。
class Hoge
{
public:
static Hoge& Instance()
{
assert(sPtr != nullptr);
return *sPtr;
}
Hoge()
{
assert(sPtr == nullptr);
sPtr = this;
}
~Hoge()
{
sPtr = nullptr;
}
private:
static Hoge* sPtr = nullptr; // ポインタだけにしておくと常時消費されるメモリ使用量が最低限で済む。
}
ライブラリを作る人が注意すること
ソースコードが提供されていない第3者が作ったライブラリコードがメモリをたくさん使う、という場面が開発中に時々あります。そして大変困ります。
これはライブラリを作る人がプログラムのデータサイズを意識せずにコーディングしているときに起こります。もしそのようなライブラリを配布してしまうと、メモリ使用量が多くなることはもちろんですし、ライブラリ開発チームの信用も下がってしまいます。
ですので、ライブラリを作る人はプログラムのデータサイズのことをぜひ意識するようにしてください。
おわり
メモリの容量的な面でもそうですが、ゲームロジックをスクリプトで書くのが主流な今、 C++ でプログラムを書く人はゲームエンジンを作る人ぐらいになりつつあり、この手の話を知らない人が今後増えていくんだろうなぁという気がしています。
筆者としてはゲームロジックの開発により集中できる現代の流れは大変嬉しいです。ゲームはお客さんの感情を動かしてなんぼですからね。
その分、この手の情報がどんどんドキュメント化されなくなっている気配も感じるので、このように記事にすることでどこかの駆け出しエンジン制作者さんのお役に立てるといいなぁと考えています。
リンク:ゲームプログラマの小話-目次