IL2CPP最適化されたコードに必要なこと
この記事は第二のドワンゴ Advent Calendar 2017の記事です。
2013年までドワンゴにいたtrapezoidです。今は居ません。
今はモバイルゲームの基盤開発エンジニアをしています。
猛烈に忘れていたので、今日はUnityにおいて手軽に少しでもパフォーマンスを出すための実装テクニックというみじんもドワンゴに関係のない話を雑にします。 あんまり実際にこれがパフォーマンス上役に立つ知識かというと非常にアレなんですが、豆知識としては面白いんじゃないか程度の気持ちで読んで下さい。
IL2CPPを使う
大前提です。 旧来、iOSではMonoによるAOTコンパイル、Android等ではMonoのVM上でスクリプトが動作していたUnityですが、 時代は変わりました。iOS 64bit対応で先行してiOS向けに導入されたIL2CPPも比較的安定した挙動を示すようになり、AndroidでもIL2CPPを利用できるようになりました。
現代のUnityでIL2CPPを使う上での懸念は殆どありません。
LINQもIL2CPPになって使えないメソッドが殆どなくなるので、MonoのGenericsの実体型の推論の気持ちを慮る必要もなくなります。IL2CPPの気持ちのほうがまあいくぶんマシなのです。
かけれる場所にはsealedをかける
IL2CPPはsealedキーワードのついたclass/methodに関して、devirtualizationを行って仮想関数呼び出しを避けてくれます。
classにおけるsealed周りの仮想関数呼び出しの挙動は一度自動変数に入った時点でsealedでもcallvirtにされていたはずなので、これに関してはIL2CPPは結構突っ込んだ最適化をかけてくれているようです。
継承による拡張とかロクなことがないので、sealedできる設計を心がけましょうという話でもあります。
var使う
sealedかけたりしても流石にinterfaceで受けてしまうと無力なので、自動変数はvarでできるだけ具体的な型は残し、最適化の余地を高めておきましょう。
そもそもvarは安価に使える広義のポリモーフィズムなので、サンプルコード以外でvarを避ける理由は無いです。
また、パフォーマンス以前の話ではありますが、早すぎる抽象化も避けましょう(副作用がなく多態性の不要なものにinterface使うのをやめよう)
構造体を使う/boxingを避ける
単純な話ですが、structを使うことでヒープの確保を避けられる上、 interfaceやobjectへのキャストを避けてboxingをされないように/boxingされない状態で取り扱うようにすることで、仮想関数呼び出しも避けられます。
neueccさんあたりのブログで出てくる話ですが、有名な話としてUnity5.5でコンパイラが更新されるまでは、foreachを使うとイテレータが破棄時にIDisposableへキャストされることでboxingが発生してしまうバグがありました。
structとinterfaceの併用は注意深くやりましょう。
duck typingを活かす
C#におけるforeachはIEnumerable/IEnumerable
せっかくstructになっていたりしても返り値がinterfaceだったりするとboxingされてしまい、devirtualizationもあまり期待できなくなるので、これを利用してinterfaceでなく具象型のままEnumeratorを返し、前述のsealed化やstruct化と組み合わせることで、仮想関数呼び出しを避けられます。
using構文は何故かIDisposableを実装していることをコンパイル時に要求されるのですが、(Unity5.5以降であれば!!!)展開されるコードはIDisposableとしてでなく、元の型のまま呼び出されることになるので、制約としてIDisposableの実装を要求しているのにすぎないです。
オブジェクト初期化子もAddというメソッドへのDuckTypingになっているので、同様の事が言えます。
readonly staticではなく、constを使う
C#におけるconstは、IL上で直接即値としてインライン展開されます。
これは参照側のコンパイルタイミングで実際の値が決定されてしまうということになるので、publicなconstを他のアセンブリに露出する形で利用することは避けたほうが良いです。
しかし、readonly staticの場合は型自体のスタティックフィールドへの参照が生まれるので、すなわちtype initializer(またはその状態確認の分岐コード)が働いてしまうということになります。
調べた範囲では、beforefieldinitの有無にかかわらずtype initializerのチェック&呼び出しが入ってるようでしたので、これに関してはIL2CPPになることで状況は少し悪化してると言えます。 constにすると型自体への参照がなくなるので、これを回避できます。そもそも単独でDLL化するようなライブラリ実装時ならともかく、Unity配下でコンパイルがかかるコードの部分にはそもそもconstのデメリットは現れません。一部の人間以外はガンガンconstを使っていきましょう。