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を使っていきましょう。
Unity 5.5以前ではGenericsクラスのコンストラクタのデフォルト値としてconstを参照してはいけない
TL;DR
- Unity 5.5以前(monoの更新前)ではGenericsクラスのコンストラクタのデフォルト値としてconstを参照してはいけない
- メンバメソッドの名前解決ができなくなる
- なぜかフィールドは名前解決ができる
- Unity 5.5以降にすることですべては解決する
詳しく
以下は通らない
public class ConstInConstructor<T> { private const int Const = 1; private int field = 0; private event Action SomeEvent; private Action SomeDelegate; public ConstInConstructor(int arg = Const) { } private void InnerLogic() { } public void Execute() { field = 1; // ok SomeDelegate(); // ok SomeEvent(); // error CS0103: The name `SomeEvent' does not exist in the current context InnerLogic(); // error CS0103: The name `InnerLogic' does not exist in the current context } }
以下は通る
public class NotConstInConstructor<T> { private int field = 0; private event Action SomeEvent; private Action SomeDelegate; public NotConstInConstructor(int arg = 1) { } private void InnerLogic() { } public void Execute() { field = 1; // ok SomeDelegate(); // ok SomeEvent(); // ok InnerLogic(); //ok } }
UnityでEditor拡張から高速にC#コードを書いたり消したりする時のお作法
TL;DR
- 書いてる最中からコンパイルされてEditor拡張が走ってるアセンブリが猛然と破棄されたりする
- EditorApplication.LockReloadAssemblies, EditorApplication.UnlockReloadAssembliesで囲め
- AssetDatabase.StartAssetEditing, AssetDatabase.StopAssetEditingで囲め
どういうことか
Unityを使ってるとそのだるさから自動生成によるコード生成を行ったり、パッケージ管理がだるすぎて思わずパッケージマネージャを自作してしまうことがあると思う。
そのような場合には高速にスクリプトからC#コードを生成したり削除したりするわけだが、Unityはせっかちなので生成してるそばからコンパイルを始めて、実行中のコードがアセンブリごと破棄されたり、不完全なコンパイルがされたり、AssetDatabaseが一時的にぶっ壊れたりする。死ね
そのような場合には適切なAPIを各位やっていく必要がある
対処
AssetDatabaseの更新をスクリプトのコード編集中だけロックしてあげればいい。ついでにアセンブリリロードもロックしておくと安心できる。ちなみに超ステートフルなAPIとして実装されてるので、きっちり対応させて呼ばないとUnityが実質固まったり、いくらまってもあたらしいアセンブリがロードされなくなったりする。死ね
我々には文明があるので、IDisposableを実装する形で実装して、usingで適切にやっていけばよい
実装
以下のようなコードを書いた上で、高速にC#コードを書いたり消したりするコードブロックをusingで囲んでいく
using System; public class LockReloadAssembliesScope : IDisposable { public bool IsDisposed { get; private set; } public LockReloadAssembliesScope() { EditorApplication.LockReloadAssemblies(); IsDisposed = false; } public void Dispose() { if (!IsDisposed) { EditorApplication.UnlockReloadAssemblies(); IsDisposed = true; } } } public class AssetEditingScope : IDisposable { public bool IsDisposed { get; private set; } public AssetEditingScope() { AssetDatabase.StartAssetEditing(); IsDisposed = false; } public void Dispose() { if (!IsDisposed) { AssetDatabase.StopAssetEditing(); IsDisposed = true; } } }
IL2CPPに優しく、かつタイプセーフにDictionary<TKey,TValue>を使う
前提
UnityのIL2CPPを使ったビルドにおいて、Dictionary<TKey, TValue>を使うとすごい勢いでバイナリサイズが大きくなっていく。
iOSにおいてはUniversalBinaryで出力を余儀なくされることが多いのと、実行バイナリがストア上では暗号化される問題もあり、ひとつの組み合わせで100KB前後の水準で、最終的なダウンロードサイズが増大する。
"Generics使わなきゃいいじゃん"みたいなのは思考停止の原人の所業なので、きちんと型を大切にしながらIL2CPPによる生成コードサイズを削るにはどうすればいいのかを考えるのが、現代に生きる文明人に求められる仕事になる。
追い込まれた時こそ、先に踏み込んで行く必要がある。
分析
まずは何故Dictionaryがこれほどまでに膨らむかを考える。Dictionaryは比較的高級なコンテナクラスなので、内部実装として多くのものを抱え込んでいる。
corefx/Dictionary.cs at master · dotnet/corefx · GitHub
さもありなんという感じである。(Unityの実装はこれではないが、参考までに。。。)
これを全部のTKey, TValueの組み合わせでジェネリクスのインスタンス化されてしまったらなすすべがないが、IL2CPPは型パラメータが参照型な場合に限っては生成したコードを共用してくれる仕組みが入っている。つまり、できる限りコードが共用されるような使い方をすれば、Dictionaryを使ってもバイナリサイズへのインパクトを少なくすることができる。
対策
値型を使う限り、どこかでコードサイズが膨らむのは避けられない。ただ、それはDictionaryみたいな高級なクラスで行われるべきではない、という理論で対策していく。 要するに任意の値型を参照型として扱えるようにして、Dictionary<TKey, TValue>に関しては実行コードが極力共有されるようにすればいい。 そうとわかれば割と簡単で、以下のような実装をしてみればいい。
public class Boxing<TValue> { public TValue Value { get; set; } public Boxing(TValue value) { this.Value = value; } public override int GetHashCode() { return Value.GetHashCode(); } public override bool Equals(object obj) { if (obj == null || !(obj is Boxing<TValue>)) { return false; } return Value.Equals(((Boxing<TValue>)obj).Value); } public static implicit operator TValue(Boxing<TValue> boxing) { return boxing.Value; } public static implicit operator Boxing<TValue>(TValue value) { return new Boxing<TValue>(value); } }
要するに、手動Boxingをできるようにする。 この方法が良いのは、相互に暗黙的キャスト演算子を定義しておくことで、変更自体は最小の量にして、リスクを下げることができること、Dictionaryとしての機能自体は今まで通りほぼフルスペックで使えること。 一応記しておくと、適用するには
Dictionary<SomeStruct, SomeClass> dictionary;
みたいな定義をまるっと
Dictionary<Boxing<SomeStruct>, SomeClass> dictionary;
に置き換えていけばいい。大体はいけるが、たまに暗黙的キャストで解決できない場合とか、もろもろあるので、適宜やっていく。 凄くダーティーハックな感じはするが、実際大規模プロダクトだとこれだけでMB単位でのコード削減が見込めるので、費用対効果はすごく良い。
トレードオフ
楽ではあるが、都度都度Boxingが走ることになるので、負荷的にはやはり厳しいものはある。 1フレームで多数呼び出すような場合なら、そのままにするなり、せめてenumをintにキャストして扱うなどの対処ですましておくのが良い。 また、Dictionary以外で、たとえばインターフェース制約がかかってる型パラメータに対して適用する場合は、当然それ用にBoxingクラスを実装する必要が出てくる。
そもそもなんで共用できるのか?むしろなぜ共用できない場合があるのか?
なんでかといえば、要するにそのジェネリクスのインスタンスのスタックサイズに差が生まれるからだ。 値型はスタックに配置するときに直接配置されるため、それぞれの型ごとにスタック上のサイズが決まる。一方で、参照型はスタック上ではほぼポインタとしてしか扱われないので、どんな型だろうと占有するスタックサイズは同一になる。 なので、値型/参照型の組み合わせによって、インスタンス化後に生成される実行コードそのものが別のものにならなければならないことになってくる。
CLRでの実行時は、この差はジェネリクスのインスタンス化時にJITコンパイルすることによって解決されているので、ビルドサイズの問題には結び付かない。しかし、IL2CPP(または、AOT)の場合には、コンパイル時にこれらをすべてインスタンス化しておかなければならないため、この問題が表面化する。
結論
こんなことしないで済むのが一番なんで、早めにビルドサイズには気を使って、やっていきましょう。
コマンドラインから使える理想のC#コードフォーマッタがほしいという気持ち
この記事は Unity 2 Advent Calendar 2015 9日目の記事です
概要
はじめましての方ははじめまして。ヒカリエのあたりでリアクティブUnityおじさんをしているtrapezoidです。Haruto Otakeとも言います。よろしくお願いいたします。
この記事は、OSXでUnityを使っているWeb上がりのゲーム基盤開発エンジニアが、Unityの(C#の)コードフォーマットを巡る悲哀をNRefactoryの力で解決したという、リアクティブプログラミングもUnityもあまり関係のない話をします。
Unityにおけるコードフォーマッタ事情
現状、"機能としては"どのプラットフォーム上のIDEも実用上十分な性能のコードフォーマッタを持っている。
ただ、Visual Studioは基本的にはなんの問題もないんだが、MonoDevelopで深刻なのが、Unityがコードフォーマット設定を上書きするという挙動のせいで、プロジェクト切り替え時などに唐突にコードフォーマット設定がぶっ飛ぶ。
気付かないで作業し続けて巨大なdiffになってるのにPull Request送った時に気付く、とかよくある。僕は基本的に基盤開発が仕事なので、一日百万回ぐらいこれが発生して発狂しそうになる。
OmniSharpならこんな問題はないけれど、OmniSharpでの開発を強制するのはさすがに自由無さ過ぎてつらい。
Unity5.3で実はこの挙動自体が治ったりしてるんだが、さすがに入れれるのはもう少し先になるし、コミットフックも仕込みたい。 それに、上記の挙動のせいで既存コードには異なるコードフォーマットが混在してしまっているので、一度まとめて何とかする必要がどのみちあった。
既存のコマンドラインベースのC#コードフォーマッタ
そうなったときにやはりコマンドラインベースでコードフォーマッタがほしい、みたいな話になるけれど、MonoDevelop/OmniSharpのコードフォーマッタはそのままコマンドラインでたたける形になってない(調べた限り)。そこで他の選択肢を探すのだが、
- MonoDevelop/OmniSharp主体の開発体制で使っている既存のフォーマット定義をなるべくそのまま利用したい、IDE上でフォーマットしたときと結果が一致するようにしたい
- コミットフックでかかるとはいえ、書いてる途中に把握のためにコードフォーマットかけることも多いし、その状況で一々差分吐かれると非常に面倒。
- クロスプラットフォームで動いてほしい
みたいなことを考え出すと、既存の選択肢だとそれぞれ色々と問題があった。
CodeFormatter
RoslynベースのMicrosoft製コードフォーマッタ。コマンドライン、今の設定を移行させるのが激しくめんどくさい。(チームのコーディング規約側を変えてやろうかと思った...) あと、Microsoft Build Tools 2015が必須なせいか、そのままだとMonoで動かなかった。上記の理由で選ばなかったので追っていない。
Artistic Style
古より存在するコードフォーマッタ。Cと名の付く言語とJavaなら全部フォーマットできますよという懐の深さを持っているが、設定はMonoDevelop/OmniSharpほど細かくはできない。 UniRxとかLINQとか使ってると、めっちゃ見辛いインデントにしてきて完全につらい気持ちになった。 基本UniRXとLINQ絡まないコード書くことのほうが少ないので、これは無し。
ないので作る
そもそもMonoDevelop/OmniSharpがどうやってるのやという話で、まああいつらのことだしどうせNRefactoryにあるんやろ、と考えて見てみたら、実際あった。
NRefactory/ICSharpCode.NRefactory.CSharp/Formatter at master · icsharpcode/NRefactory · GitHub
このへん。使い方としてはCSharpFormatterにCSharpFormattingOptionsとTextEditorOptionsを食わせて、あとはソースコードをstringでぶちこむだけ。NRefactoryはすごい。
CSharpFormattingOptionsとTextEditorOptionsは一々指定するのはない話なので、両方まとめてXmlSerializerで読み書きするとコードフォーマット定義ファイルの出来上がりです。あとはCLIアプリとしての体裁を整えるだけ。
そのうち公開したいけど、本当に設定とコードのテキストぶち込むだけなので、諸々含めてもせいぜい2桁行に収まる規模にしかならんのでやめました。 とりあえず各位気持ちで書いて気持ちでやっていってください。
以上。
次はmasakam1さんです。
Unity 5.3のCustomYieldInstructionの話
前提
Unityでは古来よりフレームを跨ぐ処理にCoroutineと呼ばれる仕組みが用いられている。 実態としてはC#に備わっているyield構文による、値の列挙を目的としたコルーチンの生成機能を利用している形になる。
仕組みとしては、上記構文を利用して生成したIEnumeratorをStartCoroutineを用いてUnityのシステム側に渡すと、毎フレーム1回、UnityがそのIEnumeratorのMoveNextを呼んでくれる。
yield returnした値はCurrentに入るわけだが、それがYieldInstructionのサブクラスであった場合のみ、そのYieldInstructionが表す非同期処理が終わるまで次のMoveNextの呼び出しを行わないようにしてくれる。
StartCoroutineはIEnumeratorを引数に取り、Coroutineを返す。CoroutineはYieldInstructionのサブクラスなので、Coroutineをyield returnすればCoroutineの中でCoroutineを待てるようになっている。
ただし、YieldInstructionは内部実装に利用されているクラスで、C#のスクリプト層から独自のYieldInstructionを実装するようなことは、今まではできなかった。(型の定義のみで何も定義されていないという、割とC#の常識破りなクラスになっている...)
CustomYieldInstruction
Unity 5.3では、独自のYieldInstructionを実装する手段として、CustomYieldInstructionというクラスが追加された。
実装方法自体は上記記事にあるように、CustomYieldInstructionを継承してkeepWaitingをoverrideするだけでいい。
本題
実は今回のCustomYieldInstructionの追加で、ドキュメントにさりげなく記載されている、わりかし重大な変更がある。(公式Blogに記事出すなら、このことにも触れてほしかった。。。)
実は、IEnumerator自体もCoroutineとしてみなして実行するようになっているのだ。
つまり、今までは
IEnumerator IncludeNestedCoroutine() { yield return StartCoroutine(SomeCoroutine()); Debug.Log("4th Frame"); } IEnumerator SomeCoroutine() { Debug.Log("1st Frame"); yield return null; Debug.Log("2nd Frame"); yield return null; Debug.Log("3rd Frame"); yield return null; }
と描いていた処理が
IEnumerator IncludeNestedCoroutine() { yield return SomeCoroutine(); Debug.Log("4th Frame"); } IEnumerator SomeCoroutine() { //省略 }
このように、StartCoroutineなしで実現できるようになった。
今まででもIEnumeratorを実装して、StartCoroutineを挟みさえすれば(冗長だが)独自YieldInstructionらしきものは作れはしていたのだが、 うっかりCurrentとして自身を返す実装をしてしまっていたりすると、StackOverflowExceptionを延々と吐くようになる。
試しにアップデートしたらいきなりStackOverflowException吐くようになってマジで面食らったので、一応共有。
対処は簡単ではあるんだけど、もうちょっと大々的に告知して欲しかったなあ。。。
MDR-1ABT買った
この記事で紹介したMDR-1RBTMK2の正当後継機、MDR-1ABTが先日発売されたので、発売日に早速購入してきた。 これでこのシリーズは3世代全部買ってる事になる。
装着感の改良
1RBTMK2よりもイヤーパッドが厚くなり、より立体感のある縫製になった。人間の人体、耳の周りは平坦ではないと思うので、それに合わせていい感じに分厚くなるように縫製されている。 あとはヘッドバンドに対してハウジングの角度が少しつく(ハウジングとスイーベル機構つなげる軸が、スイーベルの軸に対して平行でないように)ようになった為、より耳にハマるようになった。
操作性の改良
耳からはずして肩掛けして持ち歩いていると、ふとしたときにボタンが誤って押されるという自体が偶にあったのだが、1ABTから音量/曲送り/曲戻し/再生/停止は左のハウジングに搭載された静電センサで行うようになった。 タンクトップにこれだけみたいな感じの黒人スタイルだと誤動作起こしそうだけど、数日使った感じでは勝手に再生されたりするみたいなことは無いのでいい感じ。後述するがインターフェースがスッキリしたのにもだいぶ寄与していると思う
接続性の改良
microUSB充電端子のキャップが薄くなり、またカバーが柔らかい素材から硬い素材+ラッチで奥で止めてあるみたいな感じ?の可動性の高い作りになった。今まではカバー持ったままケーブル刺さないとないとすぐに戻ってしまってつらかったので、地味に良い改良。 ステレオミニジャックに関してはカバーがなくなり、右ハウジング側に分離された。防水じゃないんだしカバーあってもな・・・て思ってたし、音ゲーマーは頻繁にこれの抜き差ししたいという需要あるので、良い改良。 存在意義のわからなかったNFC有効/無効スイッチも消滅した。 前述した操作系の静電センサ化とあわせてボタン類の数は激減したので、見た目かなりスマートな感じになったという印象。
色
やっとカラーリングが増えて、シルバーも発売された。 迷わずシルバーにしました
音
LDAC端末を持ってないのでなんとも。Z3 Compactでも使えるようにしてくれないかな...