Nimでのメモリ管理を考えてみる
動機
GCあるから使いたいけど、一方でGC使うの怠いなという気持ちも有るので考えを整理したい。
どんなところでGCを使いたいのか
寿命の追跡が困難なオブジェクトの管理。
クロージャーやラムダなど変数のキャプチャが絡む機能、メソッドチェインで受け渡し続けるデータ、永続データ構造などGC無しでは辛いデータ構造など。
何でGCを使うと怠いのか
メモリのレイアウトを制御してプログラミングできないから。
DODなどCPUキャッシュに優しいデータ構造を目指す場合にGC側で勝手にメモリの場所を指定されると困る。また手動で一括でメモリを確保する際に、個別のオブジェクトをGCで追跡できない。
実際に困った例
所謂ECSを構築する際に個別のComponentをどう公開するかで悩んだ。
GCをフル活用するなら
var component = new Component
world.components.add(component)
return component// ref Component
みたいな形でref Component
型で返すのがオブジェクトの寿命問題を無視できるパターンなのだが、この形式はキャッシュに優しくない。
キャッシュに優しい形を目指すと
type world = object
components: array[256, Component]
のようにメモリに連番で配置されるレイアウトを取るべきで、そうなると個別のComponent
をGC
で追跡することは不可能になる。
なのでComponent
を可変の状態で公開したいと考えたときにはptr Component
型で公開する必要があるのだが、GC搭載言語で積極的にポインタで公開したくないよなという気持ち。
解決策その1
キャッシュに優しい形を捨ててGC側に全てを任せる。
なのでポインタで公開したいオブジェクトは全てnew Hoge
でGCに乗せてref Hoge
で公開する。
有りと言えば有り。
キャッシュに優しいうんぬんは完全にCPUに比べてメモリのクロック数が著しく遅いという現代のPCのアーキテクチャに依存した話なので速度がそこまで必要ないなら気にしなくて良いとも言える。
解決策その2
GCに頼るのを辞めて手動メモリ管理指向でバリバリにポインタを公開する。
バリバリと書いたけど実際は所有権無しの弱参照的な公開にとどめて、長期に参照したい場合はハンドルで公開するというルールで扱う。
有りと言えば有り。
キャッシュに優しい形は継続できるし、ポインタの取り扱いルールさえ統一されていればポインタで悩まされることは少なそう。
ただ手動メモリ管理方式と変わらないのでGC搭載という理念には反するかも。
好みの話
個人的には何も考えずにGCに乗せるのは好みじゃない。
ただ、全部のオブジェクトのメモリを考えて管理しろというのも怠い。
全部のオブジェクトのメモリを管理するとなると一時的なAllocator含めてプログラマ側で管理する必要があるのだが、それは面倒。
特定の関数内でしか存在しないことを保証しているオブジェクトなら態々Allocatorをプログラマが管理せずとも勝手に開放してほしい。
ヒープを使いたい場面は2つで1つはスタックに載せきれないほど大きなメモリを扱いたい。2つめは使用するメモリの量が不定の場合。どちらのケースも当てはまるのが動的配列を使いたいときだけど動的配列を使うたびに、いちいちAllocatorもセットで管理するのは正直怠い。
(あとは文字列とか)
一方でゲームオブジェクトなど長期的に管理することが明確なオブジェクトのメモリ管理は、そこまで面倒ではないと感じている。
初期化、削除タイミングは一定だしゲームは動作するモードが厳密に決まっている事が多い(戦闘画面、マップ画面、タイトル画面など)ので定期的にリセットをかけたりすることもできる。
そもそも、こういうオブジェクトは何かしらの中央集権的なシステムで管理することが多いので、そのシステムにメモリ管理の役割を渡すことで自然に扱える。
ココだけ見ると一時的なオブジェクトならGCに乗せて良くて、長期に保存したいオブジェクトはGCに載せないというのがベターなのではと思えてくる。
実際に書いていくと一時的なオブジェクトと長期的なオブジェクトの違いって何だよというので悩みそうだが、ゲーム基準だと1フレーム以上生き延びる可能性のあるオブジェクトは全部長期という判断で良いと思う。
Nimでポインタを公開する際に面倒なのが不変性の担保が出来ないという点。
const Component*
みたいな公開の仕方は出来なくてポインタで公開する以上は必ず可変な変数として公開しなくてはいけない。
対応としてはコピーして渡すか、読み取り専用のオブジェクトでラップして渡すか。
コピーは速度の面から不利で読み取り専用は手間がかかりすぎる。
もしくはRustで良くある
proc hoge(a: var Component) =
...
world.map(hoge)
みたいな形で処理するか。
この場合はマクロを使用する必要があるんだけどポインタで公開するのを避けるためだけにマクロを書くのか...。
結論
一時オブジェクトだけGCで管理して長期オブジェクトは個別に管理する。
ポインタは一時オブジェクトとして扱って保存しないという前提のもと公開する。
なのでGCは積極的に活用せずにオプションとして使うにとどめる。
なんだかGC採用言語を使う旨味を捨てている気もするがパフォーマンス的にはGCに頼らない形が最速なのとオブジェクトの管理という意味でもGCに任せ過ぎない方が自然だと感じるのでこの方針で書いていく。