Upload
alwei
View
26.426
Download
3
Embed Size (px)
Citation preview
カスタムメモリマネージャと高速なメモリアロケータについて
@aizen76
自己紹介
ハンドルネーム alweiTwitter ID @aizen76
alweiもしくはaizenのどちらかを使っています
流浪のゲームプログラマWii、DS、PSP、PS3、3DSのゲームとか作ってた
2Dゲーム好きなのに、実は3Dゲームしか作ったことがない
最近触手軍団に仲間入りして、オンラインゲームを作り始めました
アジャイルとかTDDとか勉強中
自己紹介
根っからのVimmerだったり、Opera使いだったり、Happy Hacking Keyboard使いだったり、SKK使いだったりであとは極普通のC++er
闇の軍団とかこわい
なのに気づいたら周りに沢山いたりしてガクブルしてます
本題
カスタムメモリマネージャ
メモリ確保を監視、もしくは変更する
C++においてnew演算子でメモリを確保する際に、operatorオーバーロードでメモリ動作をフックします
inline void* operator new(std::size_t size) { return memAlloc(size);}
inline void operator delete(void* deletePtr) { memFree(deletePtr);}
何が嬉しいの?
* メモリをどこで確保したのかわかる
* 残りのメモリ量がわかる
* メモリの断片化具合がわかる
* フリーストアやヒープ以外からでもメモリを取得出来る
* メモリアラインメントが指定出来る
* 状況によってメモリセクションを使い分けることが出来る
* etc...
楽なことばかりではない
メモリを管理するということは自分で責任を持つということ
メモリ関係のバグは言わば即死系なものや、メモリ破壊による追跡不能なものが多い
自分のやっていることに自信がない限りは、安易にメモリ管理しようとは思わないこと
メモリ破壊バグで丸一日潰れるくらい覚悟する必要も
コンソールゲームの世界ではメモリがカスタム出来ないと、お話にならないことが多いのでやらざるえない
カスタムメモリマネージャを使用しての様々な実用例
メモリリークを調べる
newした場所を記憶し、そのメモリがどこで確保されたかを知る
確保したメモリをリスト化し、一定感覚で情報をログとして出力させる
例えばゲームのステージ開始時と終了時をチェック
ステージ開始時と終了時で差異が出ている場合は警告として報告するなどするとすぐにリークが発覚する
エラーチェック
カスタムメモリマネージャで確保されたメモリかをチェック
メモリを確保する際にヘッダ情報を追加しておくヘッダには一意のシグニチャを付加(0xDEADC0DE等が有名)確保と開放時にこれが一致しないとエラー
■ new内部AllocHeader* header = reinterpret_cast<AllocHeader*>(allocPtr)header->signature = 0xDEADC0DE;
■ delete内部assert(header->signature == 0xDEADC0DE);
エラーチェック
メモリ確保時に確保サイズ末尾にマーカー情報を埋め込む
マーカーは二度と書き換えられないので、変更があったら不正扱いとしてエラー処理を行う配列などで確保した際にデータが壊れていないかに役立つ
■ new内部int* marker = reinterpret_cast<int*>(memAddr + size);marker = MARKER_NUMBER;
■ delete内部int* marker = reinterpret_cast<int*>(memAddr + size);assert(*marker == MARKER_NUMBER);
更にもっと高度なメモリ管理
デフラグ・メモリコンパクション
次々とメモリを確保していくと次第にメモリが断片化していく
断片化は長く起動するアプリケーションほど起きやすい断片化が緩やかに進むといつかメモリが確保出来なくなる
小さいデータが大量にあって大きいデータを確保すると、メモリの容量的には全然足りていても普通に確保出来ないことも
■ どうする? C++以外の言語ではGCで勝手にお掃除してくれる メモリの情報を再配置(コンパクション)するしかない でもどうやって?
スマートポインタでポインタをラップ
生ポインタをスマートポインタでラップして確保されたポインタの情報を連結リストにする
ポインタがスマートポインタにラップされているので、リストを辿りながら少しずつメモリの内容を移動させていく
インタフェースに制限を加えれば安全にコンパクション出来るはず
アンチャーテッドで有名なノーティドッグ社のゲームエンジンでは上記の方法で出来る限りスマートポインタを使用し、安全にメモリの内容を定期的にデフラグしている
※Game Engine Architectureより
デフラグコストの分割
ゲームはリアルタイム要求がとてもきついので、単純にデフラグすればいいというものではない
特にデフラグとはメモリブロックのコピーなのでとても遅い
断片化は即死するわけではないので、数フレームにわけて少しずつ移動させていけばいい
処理負荷が高い時などは自動的にデフラグを停止する機能があったりすれば処理落ちにも強い
メモリ管理はとても便利だが危険
メモリを管理するメリットというのは沢山あるが、ややこしさはかなり増すので必要性がなければオススメ出来ない
しかし上手くやればどんどん便利になるので、積極的に活用すべき時は活用すべし
「えーマジメモリ管理!?」「メモリ管理が許されるのは小学生までだよねー」
みたいに他のLLやVMで動く言語と比較されてバカにされても負けずにメモリのことを正確に知りちゃんと扱いましょう
高速なメモリアロケータ
メモリアロケータとは
一般的にはメモリを確保する仕組みである
通常はフリーストア(ヒープ)上から取得するものだが、メモリアロケータを自作することにより、それ以外の場所からでも自由に取得出来るようにすることが出来る
通常、mallocやnewはフリーストア(ヒープ)から取得するしかしplacement new等を使用すればそれ以外の領域からでもメモリを扱うことが出来る
工夫次第でメモリ使用量を削減したり、高速に動作するアロケータを自作するようなことも可能
様々なアロケータの紹介
スタックアロケータ
その名の通りスタックベースなアロケータ
先頭ブロックから順にメモリを取得していきますメモリの確保と開放は必ず同じ順番にならないといけない
確保されているメモリのサイズと順番がわかるため、非常に高速でかつ、無駄なメモリを使用しない
欠点はメモリを自由に開放出来ない一時的に大きなテンポラリバッファが必要な場合に有効
両端スタックアロケータ
スタックアロケータで使用する単一のメモリブロックの最下位と最上位で互いにトレードオフを行い、必要なメモリを分配しながら確保していく
Midway社が開発したHydro Thunderというレースゲームでは全てのメモリ割り当てが両端スタックアロケータになっており、最下位スタックはレベルのロードやアンロードに使われ、最上位スタックはテンポラリバッファとして使用していた
メモリの断片化が一切起こらず、非常に高速に動作していた
シングルフレームアロケータ
これもスタックアロケータを使用したアロケータ
ゲームの1フレームをメモリの寿命とする次フレームの頭で全てのメモリは開放されるので、自分でfreeやdeleteをする必要がない
寿命が固定されているので自分ではメモリを制御出来ない確保したメモリを次フレームを跨って使用してはいけない
扱いは難しいが割り切って使えば非常に高速
ダブルバッファアロケータ
シングルフレームアロケータをダブルバッファにしたもの
確保したメモリは次のフレームまで持続し、アクティブなバッファをフレームごとに切り替えて寿命がきた時に自動で開放していく
1フレームだけ長生きするので、後で確保したメモリを使用して処理させたいという時には非常に便利
自分でdeleteする必要がないのも一緒
スモールオブジェクトアロケータ
Modern C++ Designの作者、Andrei Alexandrescuさんが考案したアロケータ
小さいオブジェクトのメモリ確保に特化している小さいオブジェクトを効率よく確保しつつサイズも抑える
本当に小さいオブジェクト向けらしい具体的には32バイト程度で64バイト以上は普通に遅い?
LokiというC++ライブラリの中ではあらゆるものでこのアロケータを使用し、高速に動作しているらしい
メモリプールアロケータ
固定長サイズの領域から固定サイズのメモリを確保する
必ず固定サイズなので確保時間も開放時間も常に一定断片化の心配も一切ない
deleteを忘れてもリークすることはないが、長時間開放しないとメモリプールを圧迫し、最終的にメモリが更に必要になる
■有名な実装 Boost.Pool Efficient C++ MemoryPool
メモリプールアロケータ
メモリプールはかなり万能なアロケータ
メモリ容量を無駄に使用してしまうという欠点を除けば、newやmallocのデメリットはほぼ消しさることが出来るお手軽に使用出来るので、使わない手はない
placement newを使用することにより、実装することも出来るが自分で拡張出来るようにしておいた方が後々得することが多い
特に断片化知らずなので、断片化で悩んでいた人はこれを使って解消しましょう
実際にメモリプールを使ってみる
汎用メモリプール!!
汎用メモリプール
newで確保するものを全てメモリプールから取得メモリマネージャ内に5つ程度のメモリプールを作成しておく
メモリマネージャが自動で複数あるプールから必要なメモリサイズに応じてプールを選択する大体16バイト〜256バイトくらいまでサポートすれば十分
それ以上のメモリを確保する際は通常のnewの処理を行う
結論
汎用メモリプールが超万能
STLをバリバリ使っても「もう何も怖くない」状態
Boostだろうとメモリをフックしてしまえばどうにでもなる
みんなもメモリプールを使って
「あのライブラリがメモリ確保しまくってオセーんだよ!!」
みたいな状況から脱却しましょう
GitHubにてソースコード公開中!!
https://github.com/alwei/MemoryMaster
ご静聴ありがとうございました