21Pに誤記 誤:クラス型にする? それとも参照型? 正:クラス型にする? それとも値型?
誤記、どっちも同じ・・・
メモリと闘う者達〜GC編〜Nozomi Tanaka / @nozomin770Gotanda.unity #10 2019/1/23
View Slide
Tanaka Nozomi(タナカ ノゾミ)・ワンダープラネット株式会社 - グローバルスタジオ所属・Unity使い4年半、Cocos2d-xもはじめました・Ruby、PHPなどを使ったサーバーエンジニアでもあります・プロジェクトマネジメントとインフラの修行もはじめました・Twitter:@nozomin770
アジェンダ・1分でわかるメモリの仕組み・C#とGC・GCで死ぬ(フリーズする)ゲームを救えないのか・ボックス化、ラムダ式、クラス型と構造体、愉快な仲間たち・まとめ
これは、GCと戦う熱き勇者達の物語である
1分で分かるメモリ管理メモリには2種類の保存場所があります「スタック」という場所と「ヒープ」という場所の2つです
スタックのルール先入れ、先出し、隙間なし隙間なく積み上げるため効率がいい
ヒープのルール空いているところにうまいこと入れる隙間あり、効率はよくない
全部スタックだと効率がよいのでは?・スタックが使えるときは限られている・プリミティブ型はスタックを使えると思えばOK(object、stringは除く)・その他のほとんどの場合はヒープのメモリを使うことになる
ヒープが悪い?そういうわけじゃない
C#のメモリとの付き合い方・C#はガベージコレクション(以下GC)という仕組みでメモリの片付けを自動管理してくれている・GCが走るとき、UnityではGC終了まで処理が停止してしまう(俗に言う処理落ち)
GCの発生条件をつかもう・OSの物理メモリが足りない場合・利用可能なヒープメモリの使用量が一定のしきい値を超えた場合 なおしきい値は常に変動する(!?)・GC.Collectメソッドが呼び出された場合
GCが走る世界線を回避できないのか・・・?
GCの動作原理を支配する・C#のGCは「到達可能性」を見て回収可能な変数を見ます・「到達可能性」の変動条件→ヒープ領域へのメモリ確保(アロケート)が原因→これは変数宣言時に行われる
到達可能性の変動を抑える・ヒープ領域への確保が行われる条件- ボックス化(ボクシング)- クラス型のオブジェクト生成・上記を回避するようにコードを書くことでGCが走らない世界線にたどり着けるはずだ・・・
どうやって・・・
ボックス化とは・値型をobject型(参照型)として扱うとき、スタック上からヒープ上に値をコピーする処理を「ボックス化」(boxing)と呼びます・値型を使いながらもヒープ領域を使ってしまうため、(個々は僅かだが)無駄に重くなる・ヒープ領域へのアロケートが行われてしまう(GCを誘発する可能性が生まれる)
ボックス化を(適度に)避けたい- メソッド引数がobject型になってないか気をつける- Equalsは生で使うとobject引数が多い…- 比較対象クラスへのIEquatableインターフェース実装がおすすめ- 一部のUnityのクラスはDictionaryでボックス化が起きる- structでないクラス- またはIEquatableを実装していないクラス- UnityEngine.Objectを継承してるクラスに多い
とはいってもやりすぎは辛い・ボックス化が起きる条件は値型の取り扱いのミスに多い・int型をobject型に変換・ジェネリックは使ってOK・(Listにintを入れてもboxingされない)・GCが起きていい時はボックス化に気にする必要はない(と思ってます・・・)
クラス型オブジェクトの生成を回避・ラムダ式の展開式に気をつける・メソッドをラムダ式に入れると展開時にデリゲートをnewする・ラムダ式にメソッドを食わせる場合は大体ゴミが出る・値型以外を使っても勿論ゴミは出る・コアロジックなどループする箇所では注意・ラムダ式にメソッドを入れないのは現実的ではない
その他の回避策・事前確保によりゲーム中のアロケートを避ける- 確保時に足りないから到達可能性を見る- 確保するから到達可能性が変わってしまう- GC発生してほしくない時は確保しなきゃいいじゃん理論- あんまり現実的ではないかも。インゲーム全体を管理可能な規模ならアリ・データクラスはクラスでなく構造体で作る、とか。
クラス型にする? それとも参照型?・複製を行わないデータは構造体で作ると良い- 値型は代入時に複製が必要で、重くなり得る・「クラスの継承」や「仮想メソッド」など多態性を使いたい場合はクラスで作ると良い- というかクラスでしかできない
その他、これはどうなの編- foreachはだめ?- IL2CPPがfor文に展開してくれる- for文でボックス化が発生しないコードならOK- UniRxはだめ?- ふつーに使う分には問題ないと思います(内部コードは対策済み)- オペレータの引数に入れるラムダ式がボックス化を誘発するコードの場合、展開時にボックス化すると思います
その他、これはどうなの編- LINQはだめ?- 完全にものによる・・・- ラムダ式がある場合はラムダ式の展開式に注意- 引数がある場合、値型で送るかジェネリック型で送れるならボックス化はしないはず- LINQの内部実装でボックス化してたら重いLINQメソッドなので諦める
まとめ- メモリの管理場所「スタック」と「ヒープ」を意識する- GCの発生条件を意識する- 「到達可能性」を変化させないことで間接的にGC発生率を減らせる- 「new」「ボックス化」を抑える- ラムダ式は展開時にGCを誘発するケースに注意
参考にさせていただいたHP- https://www.slideshare.net/KMC_JP/c-91154309- http://neue.cc/2016/01/06_525.html- https://www.scriptlife.jp/contents/programming/2017/05/14/unite2017-performance/
ご清聴ありがとうございました