Dlanguage
dlang
D言語Day 6

ゲーム作る時のGCについて…

More than 5 years have passed since last update.

しばらくD言語から離れていたのですが、最近D言語熱が再燃してゲームでも作ろうと思っています。
そこでD言語でゲームを作るにあたって気を付ける必要があるGCについて、調べたことを纏めたいと思います。

D言語! D言語!

みなさんゲーム作ってますか?
D言語はサクサク書けてバグが入りにくく、コンパイル速度が速くて実行速度も速い!
ここまで聞くとゲーム開発に最適な言語と言えますね。

しかし、D言語について調べてみて
「えっ、D言語ってGCあるの…。駄目じゃん…」
と思ったC++系なプログラマさんは多いと思います。

なんでGCがあると駄目なのか

GCとはガベージコレクション。使われなくなったヒープメモリを自動で回収してくれる便利な機能で、
最近の高級言語やスクリプト言語はだいたい搭載しています。
GCが無い言語は不要なメモリを手動で解放してやらないといけなく、
メモリを解放し忘れるとメモリリークとなってしまい、大方よろしくないことが起きます。
ミスを犯すのは常に人間なので、コンピュータで自動化してミスを減らそうというのは真っ当だと言えます。

ただ、このGCのメモリ回収処理。結構重いのです。
D言語はマーク&スイープという伝統的なGCアルゴリズムが実装されています。
マーク&スイープはメモリ回収中は全スレッドの実行がブロックします。
そのためStop the Worldとも呼ばれたりします。

ゲームとGC

ゲームは60fpsでなめらかに動かそうとすると、約17ms以内にフレームを更新する必要があります。
確保しているメモリが多くなればなるほどこのGCのブロック時間が長くなり、数msになることも珍しくありません。
17msしかないのに数msもGCにブロックされたら、余裕が無いときにフレーム落ちしてしまいます。
フレーム落ちはプレイヤーのストレスに繋がるのでなるべく避けなければなりません。

最近UnityでイケイケなC#でもGCは無視できない存在で、なるべくGCを動かさないように各々努力しているそうです。
ロード中はGCが走っても仕方ないとして、ゲームプレイ中はなるべくGCを動かしたくないですね。

D言語はGCについては活発に議論されており、今どきのGCアルゴリズムを採用したりスレッドローカルに
メモリ回収したりして、今後改善されるかもしれません。

GCと上手く付き合う

オブジェクトプール

ゲームシーンで使うオブジェクトはロード中にnewしてプールしておきましょう。
ゲーム中に敵キャラが出現するところでnew Enemyしたりするのは、とてもオブジェクト指向的で
分かりやすいですが、フレーム落ちさせたくなければなるべく止めましょう。
ただ、完全にnewを排除するといろいろ大変だと思うので、頻繁にnewしているようなところを
オブジェクトプール化するのが良いと思います。

文字列

文字列は配列です。使い方によっては動的配列になってメモリ確保が行われます。
スコアやHPを表示したりするのに数値の文字列化が必要なときは
静的配列とstd.string.sformat()を使いましょう。

char[1024] buffer;
auto text = std.string.sformat(buffer, "Score:%04d", score);

こうするとbufferにスコア情報が書き込まれ、そのスライスがtextに代入されます。
text.lengthはちゃんと10になります。スライスは参照なので別途メモリ確保は行われないのです。
まあ感覚はsprintfと同じですね。

UFCS使えばこんなにもスマートに

char[1024] buffer;
auto text = buffer.sformat("Score:%04d", score);

std.string.format()というのもあって、こっちは事前の配列宣言が要らなくて便利ですが、
ヒープ確保が行われ、GCが動く要因になってしまいます。

string text = std.string.format("Score:%04d", score);

バッファメモリは手動管理?

画像やサウンドを保持するような大きいバッファメモリは参照しないのにもかかわらず
GCがスキャンするからmalloc()/free()でGCの管轄外に確保したほうが良い。
こちらに書かれていたので実験してみたら、ubyte[]ではGCの負荷が上がることはなかったです。
void*[]で試したら負荷が増大したので、druntimeのソースを見てみたら参照型で確保したメモリのみ
スキャンするようになっていました。
そちらのサイトはD1.0時代に書かれたものなので近年改善されたのでしょう。

GCを制御する

core.memoryのGCを使うとある程度制御することができます。

GC.disable() / GC.enable()

GC.disable()するとGCを止めることができます。GC.enable()で再開です。
ただ、GCを止めるとリークしているのと変わらなくなるのであまりお勧めしません。
GCがプールしているメモリが尽きると、OSからどんどんメモリ確保されます。
(メモリが本当に足りなくなるとGCが動くので厳密に言うとメモリリークではない)

GC.collect()

GC.collect()を呼ぶと明示的にメモリ回収が行われます。
下記のようにシーンの切り替え時にGC.collect()はしておきたいところです。

void loadScene(int scene) {
    // 前のシーンのゴミ回収
    GC.collect();

    // ロード処理本体
    // ...

    // ローディング処理のゴミ回収
    GC.collect();
}

GC.reserve()

GC.reserve()はOSからメモリを確保して管理領域とします。
GCはメモリが足りなるたびにOSに要求を出しますが、この処理は結構重いです。
必要な量がだいたい分かっているなら、ゲーム起動時等に必要量OSから
メモリを確保するようにするとメモリ確保を少し安定させることができます。

参考URL

D言語公式 - メモリ管理
D言語公式 - ガベージコレクション
D言語公式 - core.memory

後記

D言語は仕様が日に日に変わっているので、自分の認識が間違っている可能性があります。
もし間違っていたら指摘していただけると嬉しいです。

しかし公式に詳しい情報がほとんど書かれていたので、記事が結構びみょーになってしまいました。。。
9日目は今作ってるD言語のオレオレゲームライブラリを紹介できれば、、、と思っています。

次、7日目は@repeatedlyさん(二回目)です!