2011年6月18日土曜日

オブジェクトプールに溺れる

前の投稿で、GC付き言語ではGCを回避しないと悲しいことになると書きました。私はまだメモリ環境の厳しいところでGC付き言語によるゲーム実装を行ったことはありませんが、話によると考えずに作り捨てすると、数秒ごとに2~300ms止るようです。これだと正直ゲームになりません。

で、ゴミを出さないようなゲーム設計について。オブジェクトプールです。
原理は簡単です。オブジェクトプールという対象オブジェクトの配列を用意して、空なら新しく作り、そして空ではないなら配列からオブジェクトを取り出し、初期化します。そして不要になったら配列に戻します。
これだけです。

コレだけなんですが…魔物が潜みます。ヒューマンエラーを起こしやすいんです。
配列に戻し忘れはまだいいです。GCが回収してくれます。
問題は配列に戻したのにまだ参照している状況です。これはDangling PointerならぬDangling Objectです。死んだと思ったオブジェクトがまだ生きてるので、再利用するとBomb!
……とはなりませんが、プログラムが不整合を起こします。これもメモリリークほどではないかもしれませんが、検出が難しい類のバグです。new/delete malloc/freeと同じく、注意深く行わないといけません。

以上が一般的な話。以下は私が考えた(多分一般的ではない)テクニックです。

Dangling Objectはそのままだと検出は難しいです。ですが、一枚レイヤーをかませれば(完全ではないものの)検出できます。そのレイヤーとは、ハンドル経由でオブジェクトにアクセスすることです。
ハンドルがオブジェクトだと本末転倒なので、ハンドルは数値になります。
ハンドル=>オブジェクト変換機をレジストリと表現しますと、まず、レジストリは、オブジェクトの要求があるとオブジェクトプールからオブジェクトを引き出して、ハンドルを割り振ります。この際、すでに登録されているオブジェクトとはかぶらないハンドルを割り振ります。ここでは1が割り振られたとします。
要求したオブジェクトが不要になると、オブジェクトをレジストリに返します。レジストリはオブジェクトをプールに戻し、登録を削除しします。
例えば、ハンドル1のオブジェクトは登録が削除されたのに、ハンドル1でオブジェクトをレジストリからもってこようとしたとしましょう。当然、ハンドル1のオブジェクトは存在しないので、例外を投げることが可能になります。
ですが、お察しの通りハンドル1にオブジェクトが割り振られる可能性は0ではありません。なので、ハンドルは十分に広くとる必要があります。完全ではないといったのはこの理由です。

もうひとつオブジェクトプールのテクニックがあります。
アクターのようにライフタイム長いオブジェクトはGCに任せても問題ないこともありますがが、計算の一時オブジェクトは断じて許容できません。これを放置しておくと、数秒でGCが走るストレスフルなゲームが出来上がります。かといって、一々オブジェクトプールに返す処理を書くのはめんどくさい(ヒューマンエラーがおきやすい)。ならばどうすればいいか。

答えは、毎フレームごとにまとめて回収する、です。

計算の一時オブジェクトは本当に一時オブジェクトで、計算結果をどこかに代入した後は使われることはありません。なので生成したオブジェクトをどこか配列に蓄積しておいて、その配列に入っているオブジェクトはフレーム毎にプールに格納します。
一時オブジェクトではない場合は、プールからとってくるのではなくて、生成してGCに任せます。
私の経験上、deleteするのを忘れることはあっても、newを忘れることはあまりないので、オブジェクトを生成する手段が二つあっても、使い分けはうまくいきます。


以上です。この手の話はネットではあまり見かけませんね。
ゲーム作るなら速いしメモリ管理も自由にできるC++使いますしね。私もオブジェクトプールで気をもむくらいならそっちのほうがいいと思います。

0 件のコメント:

コメントを投稿