33
カスタムメモリマネージャと 高速なメモリアロケータについて @aizen76

カスタムメモリマネージャと高速なメモリアロケータについて

  • Upload
    alwei

  • View
    26.426

  • Download
    3

Embed Size (px)

Citation preview

Page 1: カスタムメモリマネージャと高速なメモリアロケータについて

カスタムメモリマネージャと高速なメモリアロケータについて

@aizen76

Page 2: カスタムメモリマネージャと高速なメモリアロケータについて

自己紹介

ハンドルネーム alweiTwitter ID @aizen76

alweiもしくはaizenのどちらかを使っています

流浪のゲームプログラマWii、DS、PSP、PS3、3DSのゲームとか作ってた

2Dゲーム好きなのに、実は3Dゲームしか作ったことがない

最近触手軍団に仲間入りして、オンラインゲームを作り始めました

アジャイルとかTDDとか勉強中

Page 3: カスタムメモリマネージャと高速なメモリアロケータについて

自己紹介

根っからのVimmerだったり、Opera使いだったり、Happy Hacking Keyboard使いだったり、SKK使いだったりであとは極普通のC++er

闇の軍団とかこわい

なのに気づいたら周りに沢山いたりしてガクブルしてます

Page 4: カスタムメモリマネージャと高速なメモリアロケータについて

本題

Page 5: カスタムメモリマネージャと高速なメモリアロケータについて

カスタムメモリマネージャ

Page 6: カスタムメモリマネージャと高速なメモリアロケータについて

メモリ確保を監視、もしくは変更する

C++においてnew演算子でメモリを確保する際に、operatorオーバーロードでメモリ動作をフックします

inline void* operator new(std::size_t size) { return memAlloc(size);}

inline void operator delete(void* deletePtr) { memFree(deletePtr);}

Page 7: カスタムメモリマネージャと高速なメモリアロケータについて

何が嬉しいの?

* メモリをどこで確保したのかわかる

* 残りのメモリ量がわかる

* メモリの断片化具合がわかる

* フリーストアやヒープ以外からでもメモリを取得出来る

* メモリアラインメントが指定出来る

* 状況によってメモリセクションを使い分けることが出来る

* etc...

Page 8: カスタムメモリマネージャと高速なメモリアロケータについて

楽なことばかりではない

メモリを管理するということは自分で責任を持つということ

メモリ関係のバグは言わば即死系なものや、メモリ破壊による追跡不能なものが多い

自分のやっていることに自信がない限りは、安易にメモリ管理しようとは思わないこと

メモリ破壊バグで丸一日潰れるくらい覚悟する必要も

コンソールゲームの世界ではメモリがカスタム出来ないと、お話にならないことが多いのでやらざるえない

Page 9: カスタムメモリマネージャと高速なメモリアロケータについて

カスタムメモリマネージャを使用しての様々な実用例

Page 10: カスタムメモリマネージャと高速なメモリアロケータについて

メモリリークを調べる

newした場所を記憶し、そのメモリがどこで確保されたかを知る

確保したメモリをリスト化し、一定感覚で情報をログとして出力させる

例えばゲームのステージ開始時と終了時をチェック

ステージ開始時と終了時で差異が出ている場合は警告として報告するなどするとすぐにリークが発覚する

Page 11: カスタムメモリマネージャと高速なメモリアロケータについて

エラーチェック

カスタムメモリマネージャで確保されたメモリかをチェック

メモリを確保する際にヘッダ情報を追加しておくヘッダには一意のシグニチャを付加(0xDEADC0DE等が有名)確保と開放時にこれが一致しないとエラー

■ new内部AllocHeader* header = reinterpret_cast<AllocHeader*>(allocPtr)header->signature = 0xDEADC0DE;

■ delete内部assert(header->signature == 0xDEADC0DE);

Page 12: カスタムメモリマネージャと高速なメモリアロケータについて

エラーチェック

メモリ確保時に確保サイズ末尾にマーカー情報を埋め込む

マーカーは二度と書き換えられないので、変更があったら不正扱いとしてエラー処理を行う配列などで確保した際にデータが壊れていないかに役立つ

■ new内部int* marker = reinterpret_cast<int*>(memAddr + size);marker = MARKER_NUMBER;

■ delete内部int* marker = reinterpret_cast<int*>(memAddr + size);assert(*marker == MARKER_NUMBER);

Page 13: カスタムメモリマネージャと高速なメモリアロケータについて

更にもっと高度なメモリ管理

Page 14: カスタムメモリマネージャと高速なメモリアロケータについて

デフラグ・メモリコンパクション

次々とメモリを確保していくと次第にメモリが断片化していく

断片化は長く起動するアプリケーションほど起きやすい断片化が緩やかに進むといつかメモリが確保出来なくなる

小さいデータが大量にあって大きいデータを確保すると、メモリの容量的には全然足りていても普通に確保出来ないことも

■ どうする? C++以外の言語ではGCで勝手にお掃除してくれる メモリの情報を再配置(コンパクション)するしかない でもどうやって?

Page 15: カスタムメモリマネージャと高速なメモリアロケータについて

スマートポインタでポインタをラップ

生ポインタをスマートポインタでラップして確保されたポインタの情報を連結リストにする

ポインタがスマートポインタにラップされているので、リストを辿りながら少しずつメモリの内容を移動させていく

インタフェースに制限を加えれば安全にコンパクション出来るはず

アンチャーテッドで有名なノーティドッグ社のゲームエンジンでは上記の方法で出来る限りスマートポインタを使用し、安全にメモリの内容を定期的にデフラグしている

※Game Engine Architectureより

Page 16: カスタムメモリマネージャと高速なメモリアロケータについて

デフラグコストの分割

ゲームはリアルタイム要求がとてもきついので、単純にデフラグすればいいというものではない

特にデフラグとはメモリブロックのコピーなのでとても遅い

断片化は即死するわけではないので、数フレームにわけて少しずつ移動させていけばいい

処理負荷が高い時などは自動的にデフラグを停止する機能があったりすれば処理落ちにも強い

Page 17: カスタムメモリマネージャと高速なメモリアロケータについて

メモリ管理はとても便利だが危険

メモリを管理するメリットというのは沢山あるが、ややこしさはかなり増すので必要性がなければオススメ出来ない

しかし上手くやればどんどん便利になるので、積極的に活用すべき時は活用すべし

「えーマジメモリ管理!?」「メモリ管理が許されるのは小学生までだよねー」

みたいに他のLLやVMで動く言語と比較されてバカにされても負けずにメモリのことを正確に知りちゃんと扱いましょう

Page 18: カスタムメモリマネージャと高速なメモリアロケータについて

高速なメモリアロケータ

Page 19: カスタムメモリマネージャと高速なメモリアロケータについて

メモリアロケータとは

一般的にはメモリを確保する仕組みである

通常はフリーストア(ヒープ)上から取得するものだが、メモリアロケータを自作することにより、それ以外の場所からでも自由に取得出来るようにすることが出来る

通常、mallocやnewはフリーストア(ヒープ)から取得するしかしplacement new等を使用すればそれ以外の領域からでもメモリを扱うことが出来る

工夫次第でメモリ使用量を削減したり、高速に動作するアロケータを自作するようなことも可能

Page 20: カスタムメモリマネージャと高速なメモリアロケータについて

様々なアロケータの紹介

Page 21: カスタムメモリマネージャと高速なメモリアロケータについて

スタックアロケータ

その名の通りスタックベースなアロケータ

先頭ブロックから順にメモリを取得していきますメモリの確保と開放は必ず同じ順番にならないといけない

確保されているメモリのサイズと順番がわかるため、非常に高速でかつ、無駄なメモリを使用しない

欠点はメモリを自由に開放出来ない一時的に大きなテンポラリバッファが必要な場合に有効

Page 22: カスタムメモリマネージャと高速なメモリアロケータについて

両端スタックアロケータ

スタックアロケータで使用する単一のメモリブロックの最下位と最上位で互いにトレードオフを行い、必要なメモリを分配しながら確保していく

Midway社が開発したHydro Thunderというレースゲームでは全てのメモリ割り当てが両端スタックアロケータになっており、最下位スタックはレベルのロードやアンロードに使われ、最上位スタックはテンポラリバッファとして使用していた

メモリの断片化が一切起こらず、非常に高速に動作していた

Page 23: カスタムメモリマネージャと高速なメモリアロケータについて

シングルフレームアロケータ

これもスタックアロケータを使用したアロケータ

ゲームの1フレームをメモリの寿命とする次フレームの頭で全てのメモリは開放されるので、自分でfreeやdeleteをする必要がない

寿命が固定されているので自分ではメモリを制御出来ない確保したメモリを次フレームを跨って使用してはいけない

扱いは難しいが割り切って使えば非常に高速

Page 24: カスタムメモリマネージャと高速なメモリアロケータについて

ダブルバッファアロケータ

シングルフレームアロケータをダブルバッファにしたもの

確保したメモリは次のフレームまで持続し、アクティブなバッファをフレームごとに切り替えて寿命がきた時に自動で開放していく

1フレームだけ長生きするので、後で確保したメモリを使用して処理させたいという時には非常に便利

自分でdeleteする必要がないのも一緒

Page 25: カスタムメモリマネージャと高速なメモリアロケータについて

スモールオブジェクトアロケータ

Modern C++ Designの作者、Andrei Alexandrescuさんが考案したアロケータ

小さいオブジェクトのメモリ確保に特化している小さいオブジェクトを効率よく確保しつつサイズも抑える

本当に小さいオブジェクト向けらしい具体的には32バイト程度で64バイト以上は普通に遅い?

LokiというC++ライブラリの中ではあらゆるものでこのアロケータを使用し、高速に動作しているらしい

Page 26: カスタムメモリマネージャと高速なメモリアロケータについて

メモリプールアロケータ

固定長サイズの領域から固定サイズのメモリを確保する

必ず固定サイズなので確保時間も開放時間も常に一定断片化の心配も一切ない

deleteを忘れてもリークすることはないが、長時間開放しないとメモリプールを圧迫し、最終的にメモリが更に必要になる

■有名な実装 Boost.Pool Efficient C++ MemoryPool

Page 27: カスタムメモリマネージャと高速なメモリアロケータについて

メモリプールアロケータ

メモリプールはかなり万能なアロケータ

メモリ容量を無駄に使用してしまうという欠点を除けば、newやmallocのデメリットはほぼ消しさることが出来るお手軽に使用出来るので、使わない手はない

placement newを使用することにより、実装することも出来るが自分で拡張出来るようにしておいた方が後々得することが多い

特に断片化知らずなので、断片化で悩んでいた人はこれを使って解消しましょう

Page 28: カスタムメモリマネージャと高速なメモリアロケータについて

実際にメモリプールを使ってみる

Page 29: カスタムメモリマネージャと高速なメモリアロケータについて

汎用メモリプール!!

Page 30: カスタムメモリマネージャと高速なメモリアロケータについて

汎用メモリプール

newで確保するものを全てメモリプールから取得メモリマネージャ内に5つ程度のメモリプールを作成しておく

メモリマネージャが自動で複数あるプールから必要なメモリサイズに応じてプールを選択する大体16バイト〜256バイトくらいまでサポートすれば十分

それ以上のメモリを確保する際は通常のnewの処理を行う

Page 31: カスタムメモリマネージャと高速なメモリアロケータについて

結論

汎用メモリプールが超万能

STLをバリバリ使っても「もう何も怖くない」状態

Boostだろうとメモリをフックしてしまえばどうにでもなる

みんなもメモリプールを使って

「あのライブラリがメモリ確保しまくってオセーんだよ!!」

みたいな状況から脱却しましょう

Page 32: カスタムメモリマネージャと高速なメモリアロケータについて

GitHubにてソースコード公開中!!

https://github.com/alwei/MemoryMaster

Page 33: カスタムメモリマネージャと高速なメモリアロケータについて

ご静聴ありがとうございました