Slide 1

Slide 1 text

メモリと闘う者達 〜GC編〜 Nozomi Tanaka / @nozomin770 Gotanda.unity #10 2019/1/23

Slide 2

Slide 2 text

Tanaka Nozomi(タナカ ノゾミ) ・ワンダープラネット株式会社 - グローバルスタジオ所属 ・Unity使い4年半、Cocos2d-xもはじめました ・Ruby、PHPなどを使ったサーバーエンジニアでもあります ・プロジェクトマネジメントとインフラの修行もはじめました ・Twitter:@nozomin770

Slide 3

Slide 3 text

アジェンダ ・1分でわかるメモリの仕組み ・C#とGC ・GCで死ぬ(フリーズする)ゲームを救えないのか ・ボックス化、ラムダ式、クラス型と構造体、愉快な仲間たち ・まとめ

Slide 4

Slide 4 text

これは、GCと戦う 熱き勇者達の物語である

Slide 5

Slide 5 text

1分で分かるメモリ管理 メモリには2種類の保存場所があります 「スタック」という場所と 「ヒープ」という場所の2つです

Slide 6

Slide 6 text

スタックのルール 先入れ、先出し、隙間なし 隙間なく積み上げるため効率がいい

Slide 7

Slide 7 text

ヒープのルール 空いているところにうまいこと入れる 隙間あり、効率はよくない

Slide 8

Slide 8 text

全部スタックだと効率がよいのでは? ・スタックが使えるときは限られている ・プリミティブ型はスタックを使えると思えばOK (object、stringは除く) ・その他のほとんどの場合はヒープのメモリを使うことになる

Slide 9

Slide 9 text

ヒープが悪い? そういうわけじゃない

Slide 10

Slide 10 text

C#のメモリとの付き合い方 ・C#はガベージコレクション(以下GC)という仕組みで メモリの片付けを自動管理してくれている ・GCが走るとき、UnityではGC終了まで処理が停止してしまう (俗に言う処理落ち)

Slide 11

Slide 11 text

GCの発生条件をつかもう ・OSの物理メモリが足りない場合 ・利用可能なヒープメモリの使用量が一定のしきい値を超えた場合  なおしきい値は常に変動する(!?) ・GC.Collectメソッドが呼び出された場合

Slide 12

Slide 12 text

GCが走る世界線を 回避できないのか・・・?

Slide 13

Slide 13 text

GCの動作原理を支配する ・C#のGCは「到達可能性」を見て回収可能な変数を見ます ・「到達可能性」の変動条件 →ヒープ領域へのメモリ確保(アロケート)が原因 →これは変数宣言時に行われる

Slide 14

Slide 14 text

到達可能性の変動を抑える ・ヒープ領域への確保が行われる条件 - ボックス化(ボクシング) - クラス型のオブジェクト生成 ・上記を回避するようにコードを書くことでGCが走らない世界線にたどり 着けるはずだ・・・

Slide 15

Slide 15 text

どうやって・・・

Slide 16

Slide 16 text

ボックス化とは ・値型をobject型(参照型)として扱うとき、スタック上からヒープ上に値を コピーする処理を「ボックス化」(boxing)と呼びます ・値型を使いながらもヒープ領域を使ってしまうため、(個々は僅かだが) 無駄に重くなる ・ヒープ領域へのアロケートが行われてしまう(GCを誘発する可能性が 生まれる)

Slide 17

Slide 17 text

ボックス化を(適度に)避けたい - メソッド引数がobject型になってないか気をつける - Equalsは生で使うとobject引数が多い… - 比較対象クラスへのIEquatableインターフェース実装がお すすめ - 一部のUnityのクラスはDictionaryでボックス化が起きる - structでないクラス - またはIEquatableを実装していないクラス - UnityEngine.Objectを継承してるクラスに多い

Slide 18

Slide 18 text

とはいってもやりすぎは辛い ・ボックス化が起きる条件は値型の取り扱いのミスに多い ・int型をobject型に変換 ・ジェネリックは使ってOK ・(Listにintを入れてもboxingされない) ・GCが起きていい時はボックス化に気にする必要はない (と思ってます・・・)

Slide 19

Slide 19 text

クラス型オブジェクトの生成を回避 ・ラムダ式の展開式に気をつける ・メソッドをラムダ式に入れると展開時にデリゲートをnewする ・ラムダ式にメソッドを食わせる場合は大体ゴミが出る ・値型以外を使っても勿論ゴミは出る ・コアロジックなどループする箇所では注意 ・ラムダ式にメソッドを入れないのは現実的ではない

Slide 20

Slide 20 text

その他の回避策 ・事前確保によりゲーム中のアロケートを避ける - 確保時に足りないから到達可能性を見る - 確保するから到達可能性が変わってしまう - GC発生してほしくない時は確保しなきゃいいじゃん理論 - あんまり現実的ではないかも。インゲーム全体を管理可能な 規模ならアリ ・データクラスはクラスでなく構造体で作る、とか。

Slide 21

Slide 21 text

クラス型にする? それとも参照型? ・複製を行わないデータは構造体で作ると良い - 値型は代入時に複製が必要で、重くなり得る ・「クラスの継承」や「仮想メソッド」など多態性を使いたい場合はクラスで 作ると良い - というかクラスでしかできない

Slide 22

Slide 22 text

その他、これはどうなの編 - foreachはだめ? - IL2CPPがfor文に展開してくれる - for文でボックス化が発生しないコードならOK - UniRxはだめ? - ふつーに使う分には問題ないと思います(内部コードは対策済 み) - オペレータの引数に入れるラムダ式がボックス化を誘発する コードの場合、展開時にボックス化すると思います

Slide 23

Slide 23 text

その他、これはどうなの編 - LINQはだめ? - 完全にものによる・・・ - ラムダ式がある場合はラムダ式の展開式に注意 - 引数がある場合、値型で送るかジェネリック型で送れるなら ボックス化はしないはず - LINQの内部実装でボックス化してたら重いLINQメソッドなの で諦める

Slide 24

Slide 24 text

まとめ - メモリの管理場所「スタック」と「ヒープ」を意識する - GCの発生条件を意識する - 「到達可能性」を変化させないことで間接的にGC発生率を減ら せる - 「new」「ボックス化」を抑える - ラムダ式は展開時にGCを誘発するケースに注意

Slide 25

Slide 25 text

参考にさせていただいた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/

Slide 26

Slide 26 text

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