Unite 2017での講演「パフォーマンス向上のためのスクリプトのベストプラクティス」の自分用まとめです。スライドだけだと結構情報落ちてるなと思って作りました。
講演が英語だったので(同時通訳はありましたが)英語できない私には追いづらくて、また内容を完全に理解できていないところもありますのでいろいろ至らないかもしれません。個人のメモとしてご容赦ください。こいつおかしいぞと思ったら動画やスライドを確認してみてもらえるとありがたいです。本当にすまない。
(公開資料)
スライド
note付きスライド
目次
.Net 4.6とC# 6.0
.Net 4.6とC# 6.0の話。.Net 4.6の新機能はすべてサポートし、.Net Standard 2.0もサポートしていくとのこと。
IL2CPPも.Net 4.6をサポートしていくよう。
GCは、段階を踏んで改良していき、安定してきたら搭載するみたい。
.Net 4.6の難点としては、コアライブラリが多くなってメモリ消費が増えるところのようです。モバイルゲームなどではマイナスになるかも。
でもモバイル端末もすごい勢いで性能上がってるので、時間が解決してくれる問題だとは思います。…解決してくれるかな…(FGOのほうを見やりながら)
.Net 4.6はまだUnity 5.6にも標準搭載されていないようで(Api Compatibility Levelに出てこなかった)、試す場合は自分でEditorをアップデートする必要があります。
5.6向けだとこちらになるのでしょうか。
Upgraded Mono/.Net in Editor and Some Players on 5.6.0b5
この講演の説明欄には
本セッションではUnity 5.5におけるコンパイラー変更後のC#プログラミングについてお話しします
とありまして、5.5でコンパイラだけMono 4.4に上がっているみたいです。UNITY 5.5がリリースされました!の下の方に、「Mono C# コンパイラー が Mono4.4 にアップグレードされました」とあります。
アンマネージドコードとMarshalling
マネージドコードとアンマネージドコード
マネージドコード(普通のC#コードなど)とマネージドコード(CのDLLなど、C#の管理から外れたコード。所謂ネイティブプラグイン)とのデータの受け渡しの話。
これを使うときは["DllImport("YourCode")]のように、DllInportを使います。呼び出されるdllは、Assets/Plugins/の下にx86やx86_64などのプラットフォームごとに分けて配置。
C++のコードをinvokeするならextern "C"を使うとことのです。
またC言語が例外(Exception)に対応していないので、使用するのは避けた方がよいとも言っていました。
Marshalling
C#のパラメータをアンマネージドコード(C)に渡すときの動作の話です。
これを行うとき、基本的にはコピーが発生するとのこと。またstringなどのクラスは、Cに対応したデータの変換も必要になります。
C#の変数にはには値型と参照型の二種類があります。値型はint, floatなどで、変数にそのまま値が入っているもの。参照型はstringなど、実際の値への参照を保持しているものになります。
値型はそのまま渡っている感じですが、参照型を渡すときには構造体に変換してコピーなどの手間が出るのかな。
そういうわけでマネージドコード側で参照(リファレンス)が取れなくなってしまうよ、C#のメモリ管理、ライフサイクルから外れるよと。
でもその後、「C側でメモリを直接破棄することができてしまい、そうするとGCが走ってしまう」みたいなことを言ってたりした気もするので何か渡せる方法があるのかもしれない。
Marshallingに関しては、パフォーマンスも悪いし常にコピーするからあまりオススメしないとも言っていました
パフォーマンスを求めてネイティブコードを書くことに対して、多くの場合はC#で必要十分なパフォーマンスが出るはず、とのこと。
どちらがいいかについてはもう場合によるのでしょうけど、確かに「C#よりCで(ネイティブプラグインで)書いた方が速い」信仰はあると思うので、ちゃんと実測してやっていきたいところです。
ベストプラクティス
ボクシングについて
殴り合う奴ではなく。値型の変数を参照型に代入するときに、自動で値型→参照型に変換してくれる機能です。
例えば次のようなコードがあるとすると、
IL2CPPで生成されるコードは次のようになります。
flort型の変数xが、Debug.Logに渡されるところでBoxingが発生してるのですね。仕方ないっちゃそうですが、このような細かい変換が走ることには気を配った方がよいのでしょう。
Foreachは避けるべきか
みたいなforeachがあったとき、IL2CPPでは
と普通のfor文に展開してくれるからどっちでもいいですよみたいな。
ただし値型の場合は、Boxingからのメモリアロケーションが発生してしまうので注意すること。それを避けるための条件は次の通り。
- 値型のコレクションの場合、コレクションと列挙子はジェネリックインターフェイスIEnumerable <T>とIEnumerator <T>を実装すること。
- 列挙子の実装は、クラスではなく構造体で行うこと。
- コレクションにはGetEnumeratorという名前のパブリックメソッドがあり、戻り値の型は列挙体の構造体とすること
このあたりの注意事項は、自前でコレクション型を実装するときのものでしょうか。ちゃんと構造体で返すようにすればオートボクシングが発生しないってことなのかな。
System.Collections.Generic.List <T>、System.Collections.Generic.Dictionary <T>、およびSystem.Collections.Generic.HashSet <T>はガイドラインに従っているとあるので、使うぶんには気にしなくてよいのかもしれない。
次のスライドからの例を見ると、自作コレクションからint型で取り出しつつ回すときみたいな感じです。
foreachの問題点についてはこちらのブログにもあることで、これが解消されてるってことなんだと思います。
C#のGCゴミとUnity(5.5)のコンパイラアップデートによるListのforeach問題解決について
classが良いのかstructが良いのか
ただメンバ変数を保持する程度が目的のクラスと構造体はどちらを使うべきか、だと思うんですが…結局どっちがいいって話になったのかよくわからなかった…
struct PointStruct { public int x; public int y; }
class PointClass { public int x; public int y; }
みたいなクラスと構造体の比較です。
二つの違いは生成時にclassはnewobj、structはinitobjが走っているところみたいなのですが。
stringを作るときにはStringBufferを使え
stringを操作するといちいちコピーが発生するから、StringBufferを活用すると良いです。
マテリアルなどの値を取得したらキャッシュしておいたほうが良いとも言っていました。
GCは6回実行した方がいい? 7回?
結論としては、基本的に1回で十分。
そもそもそういった議論があることを知りませんでした…どうも複数回呼び出すと、プラットフォームによって挙動が変わるところもあるみたいで、それによって6回が最適、いや7回だ、って話になるとのこと。
使用メモリが多くなるとGCは一部のメモリを解放するが、そのときの動作に差が生まれるので、そもそも大量のメモリを確保しないようにしろって話かなと。
逆にいえば、プラットフォームによっては6回7回実行することが最適なのでしょうか。うーん…
UnityEngineの隠れたファンクションに注意
具体的にはVector.zeroは固定値を返してるわけじゃなくnew Vector3(0,0,0)を都度返しているので注意
GameManagerパターンについて
全体を管理するクラス、GameManagerをSingletonとして用意するときの話。だが実体はEditorの罠。
Singletonの作り方が、単一のGameObjectを用意しても遅い、static変数に格納するパターンでも遅い。何故だ。何故か。
正解は、UnityEditorのときだけ動いているコードがあった。
あっはい…
得られる教訓は、「パフォーマンスを測定するときはビルドしてやろう」でした。
C#6.0について
C# 6.0についての話。アップデートされて何が変わるか、みたいなところでしょうか。
? operator
「? operator」って何かと思ったら、Null条件演算子なるもののよう。
NullReferenceExceptionが発生するような場面でも例外が発生せず、式の結果としてNullが戻るみたい。つまりメソッドチェーンで使いやすくなる。素敵!
(でも調子に乗ると、チェーンとのどのタイミングでnullになってるかわからなくて泥沼りそう)
その他
- MonoBehaviourとclass
特に変化無いとのこと。参考リンクが挙げられているけど、あんまり関係ないような… -
Sealedは使っていった方がいい
sealedは継承を禁止にするもので、通常はVirtualTableに乗るけどSealedだと直コールになるから良い。 -
Staticは変わらない
-
Unsafeもstructを使うなら変わらない
-
LINQはオーバーヘッドがある。シュガーシンタックス。
-
Genericもオーバーヘッドが生成される。
LINQとGenericは結構好きなのでむしろ積極的にやっていきたい(特にGeneric)んですが、オーバーヘッドになるのも分かるので…
自分が書くときは基本的には書きやすさ優先していって、要所要所で最適化が必要なときに判断していく感じかなと思います。
それで最後のまとめとしては次のように。
- C#のキーワードは「隠れたオーバーヘッドとアロケーション」
- 値型を使って、stringなどを避ける
- IL2CPPのコードをチェックする
- プロファイルしよう。
やはり細かいところをケアしていく話になるよなあ…と改めて思いました。