$30 off During Our Annual Pro Sale. View Details »

UniNativeLinq - Unity2018時代のNativeArray<T>用LINQ

pCYSl5EDgo
October 23, 2019

UniNativeLinq - Unity2018時代のNativeArray<T>用LINQ

Unity2018.1から登場したNativeArrayにより、プログラマーはGC管理領域外の連続的メモリを扱えるようになった。
しかし、NativeArrayは構造体であるが故に既存のLINQ to Objectsではアロケーション地獄になってしまう。
この問題を解決しうるライブラリがUniNativeLinqである。
https://github.com/pCYSl5EDgo/UniNativeLinq-EditorExtension

pCYSl5EDgo

October 23, 2019
Tweet

Other Decks in Programming

Transcript

  1. LINQ to NativeArray<T> roppongi.unity #5 @pCYSl5EDgo

  2. もうすぐUnity 2018.4のみLTS 時代は つまり

  3. C#7.3時代のUnity 構造体(struct)の扱いが飛躍的に向上 ◦Unity.Mathematics名前空間 ◦ Vector3とほぼ等価なfloat3 ◦Unity.Job名前空間 ◦ PLinqやValueTaskと互換的なC# Job System

    ◦Unity.Collections名前空間 ◦ 配列と互換的なNativeArray<T>
  4. NativeArray<T> where T : struct (実機上では)GCが一切走らない配列的な構造体 (IL2CPPビルドすれば)高速に動作する 基本的な配列を受け取るAPIに対して、 今後NativeArrayを受け取る口が用意される ※Unity2018.4ではTにboolやcharを使えない(2019.1からは可能)

  5. IEnumerable<T>の陥穽 LINQ IEnumerable<T> Where<T>(this IEnumerable<T> collection, Func<T, bool> predicate) Whereなどの呼び出し毎にGC

    Alloc発生 ゆえにインゲームではLINQ禁止が多い ※抽象クラスIterator<TSource>を継承した WhereArrayIterator<TSource>などが実際の戻り値の型
  6. IEnumerable<T>の陥穽 GetEnumerator IEnumerator<T> IEnumerable<T>.GetEnumerator(); foreachする度にGetEnumeratorの戻り値分GC Alloc必須 わたしはつらい! たえられない!

  7. LINQ to NativeArray<T> UniNativeLinq named by Decoc@Ash_Yin Core MIT License

    : コア機能に限定、拡張メソッドなしunitypackage配布 Full MIT License : 全APIを自由に利用可能 unitypackage配布 Editor Extension GPLv3 GPLv3:必要なAPIだけに絞れる 全て無料で利用可能 unitypackage形式とプロプライエタリでも使えるEditor Extension → BOOTHやAsset Storeに出す可能性がある
  8. UniNativeLinq入手先(2019/10/23時点) GitHub – Free! MITとGPLv3で配布 https://github.com/pCYSl5EDgo/UniNativeLinq-EditorExtension

  9. インストール方法 GitHub版(1/3) (プロジェクトのパス)/Packages/manifest.jsonを開く

  10. インストール方法 GitHub版(2/3) dependenciesに "uninativelinq": "https://github.com/pCYSl5EDgo/UniNativeLinq-EditorExtension.git", と一行追加

  11. インストール方法 GitHub版(3/3) メニューのUniNativeLinq/Import/Import UniNativeLinq Essential Resources TMPのように 最初に必要なリソースをimport

  12. 使用方法 / asmdef参照 Plugins/UNL/Settings/UniNativeLinqDll.dllを参照して使用

  13. ここが嬉しい UniNativeLinq(1/5) using文との相性問題を解決可能 従来 using(var array = new NativeArray<int>(10, Allocator.Temp))

    array[0] = 10; // using変数は書き換え不能 コンパイルエラー UniNativeLinq using(var array = new NativeArray<int>(10, Allocator.Temp)) array.AsRefEnumerable()[0] = 10;
  14. ここが嬉しいUniNativeLinq(2/5) インデクサアクセスの戻り値が参照 従来 // NativeArray<Matrix4x4> array; var item = array[0];

    item[0, 3] = 10f; array[0] = item; UniNativeLinq // NativeArray<Matrix4x4> array; array.AsRefEnumerable()[0][0, 3] = 10f;
  15. ここが嬉しいUniNativeLinq(3/5) byte[]への変換が簡単 従来 // NativeArray<Matrix4x4> array; unsafe{ byte* ptr =

    (byte*)array.GetUnsafePtr(); int count = array.Length * sizeof(Matrix4x4); byteArray = new byte[count]; fixed(byte* dst = &byteArray[0]){ UnsafeUtility.MemCpy(dst, ptr, count); } } UniNativeLinq // byte[] byteArray; byteArray = array .AsRefEnumerable() .Cast<Matrix4x4, byte>() .ToArray();
  16. ここが嬉しいUniNativeLinq(4/5) 基本的に速い 30%高速化

  17. ここが嬉しいUniNativeLinq(5/5) C#7.3環境でもunmanagedな総称型のポインタをある程度扱える 従来 var ptr = ((int, int)*)UnsafeUtility.Malloc(8, Allocator.Temp); //

    C#7.3でエラー UniNativeLinq var ptr = NativeEnumerable.Create<(int, int)>(1, Allocator.Temp).Ptr; ※明示的に型名を書くとコンパイルエラーになるが、varならOK! C#コンパイラがゆるがば過ぎて少し心配になる……ならない?
  18. 詳細設定(1/3) メニュー/UniNativeLinq/Open Settings “Generate DLL”ボタンをクリックして Assets/Plugins/UNL/Settings/UniNativeLinqDll.dll上書き

  19. 詳細設定(2/3) 必要なEnumerableのみを選択 不要なAPIを含まない 最適なコードサイズ & 短いコンパイル時間

  20. 詳細設定(3/3) Concat, Zip, Join, Union, … 2コレクションを引数に取るAPI 組み合わせを限定可能

  21. バージョンアップ方法 一旦Unityエディタを終了 Packages/manifest.jsonを開いて1箇所を削除 lock/uninativelinq以下 エディタ再起動後メニューのUniNativeLinq/Import/ Import UniNativeLinq Essential Resourcesで再インポート Alt

    + Shift + Dからの”Generate DLL”
  22. アンインストール方法 一旦Unityエディタを終了 Assets/Plugins/UNLフォルダを削除 Packages/manifest.jsonを開いて2箇所を削除 dependencies/uninativelinq lock/uninativelinq以下  エディタ再起動でアンインストール完了

  23. CI/CD関連の余談 https://github.com/pCYSl5EDgo/UniNativeLinq-Core 素 https://github.com/pCYSl5EDgo/UniNativeLinq-EditorExtension-Test テストコード実行、unitypackage作成 https://github.com/pCYSl5EDgo/UniNativeLinq-EditorExtension UPM用リポジトリ GitHub Actions is

    good.
  24. 謝辞(1/3) Jonathan Skeet 様 テストコードは主にEduLinqを手直しして作成 立原慎也(@Decoc)様 UniNativeLinqの命名者 @drfters様 Editor拡張のUI部分について

  25. 謝辞とクレジット表記(2/3) @adarapata様主催のゆに茶会scene 6参加者の方々 @adarapata, @monry, @TANY_FMPMD, @taptappun, @TEST_H_, @mid_zzz (アルファベット順)

    Assets以下にデータとしてDLLを含める方法について @toriae_zu_様 登壇に使用したアバターである「ツバキ」の作者 https://hub.vroid.com/characters/4054644065668079297/models/7173510333740850936
  26. 謝辞とクレジット表記(3/3) @halfsode_様 「バ美声β」使用 https://halfsode.booth.pm/items/1177717 @baku_dreameater 「VMagic Mirror」使用 https://booth.pm/ja/items/1272298

  27. おまけ(1/27) APIの基本設計(IRefEnumerable) interface IRefEnumerable<TEnumerator, T> : IEnumerable<T> where T :

    unmanaged where TEnumerator : struct, IRefEnumerator<T> { TEnumerator GetEnumerator(); }
  28. おまけ(2/27) APIの基本設計(IRefEnumerable) TEnumerator GetEnumerator(); 戻り値の型が構造体だからGC.Allocはゼロ! SelectやWhereなどはunmanaged heap確保もゼロ! ※GetEnumerator()を呼ぶまで遅延評価 本家LINQと評価タイミングが違うので注意

  29. おまけ(3/27) API基本設計(IRefEnumerable) コレクションの⾧さは基本的にlongで管理 UnsafeUtility : Unityが提供するunmanagedメモリAPI群 ・void* Malloc(long, Allocator) NativeArray<T>のコンストラクタ

    ・new NativeArray<T>(int, Allocator, NativeArrayOptions) なんでintに制限するの……
  30. おまけ(4/27) API基本設計(IRefEnumerable) bool CanFastCount() O(1)で⾧さを得られる bool CanIndexAccess() O(1)で要素にアクセスできる ToArray/ToNativeArray/ToNativeEnumerable Any/Count/LongCount

    よく使うのは最初から含めた
  31. おまけ(5/27) APIの基本設計(IRefEnumerator) interface IRefEnumerator<T> : IEnumerator<T> where T : unmanaged

    { ref T Current { get; } bool MoveNext(); ref T TryGetNext(out bool success); bool TryMoveNext(out T value); }
  32. おまけ(6/27) APIの基本設計(IRefEnumerator) ref T Current { get; } 基本 foreach(ref

    var item in collection)と書くのに必須 構造体のコピーコストを避けられる
  33. おまけ(7/27) APIの基本設計(IRefEnumerator) bool TryMoveNext(out T value); interface越しに呼び出しをすると MoveNextメソッド/Currentプロパティと2回呼び出す 仮想関数テーブルを2回ルックアップするのは無駄 では1回にまとめようと目的の下に生まれたAPI

  34. おまけ(8/27) APIの基本設計(IRefEnumerator) ref T TryGetNext(out bool success) TryMoveNextはout引数valueに必ず書き込む 無駄なコピーコスト→ref Tならば参照を返すだけ

    巨大構造体(Matrix4x4等)において TryGetNextの方が性能的に優秀(✗書き心地)
  35. おまけ(9/27) APIの基本設計(IRefAction/IRefFunc) 一例 public struct SelectEnumerable<TPrevEnumerable, TPrevEnumerator, TPrev, T, TAction>

    : IRefEnumerable<SelectEnumerable<TPrevEnumerable, TPrevEnumerator, TPrev, T, TAction>.Enumerator, T>, IEnumerable<T>, IEnumerable where TPrevEnumerable : struct, IRefEnumerable<TPrevEnumerator, TPrev> where TPrevEnumerator : struct, IRefEnumerator<TPrev> where TPrev : unmanaged where T : unmanaged where TAction : struct, IRefAction<TPrev, T> 型と制約が⾧い!⾧過ぎる!
  36. おまけ(10/27) APIの基本設計(IRefAction/IRefFunc) SelectEnumerable`5の注目点は where TAction where struct, IRefAction<TPrev, T> interface

    IRefAction<T0, T1>{void Execute(ref T0 arg0,ref T1 arg1)} 構造体の型に処理内容を紐付ける
  37. おまけ(11/27) APIの基本設計(IRefAction/IRefFunc) 値型と処理を紐付けて型名に応じた振る舞いをさせる Policy-based Design C#では値型な総称型の特性によりインライン最適化がかかる 参照型だと型が一意に定まらないので仮想関数呼び出しも加わり遅い

  38. おまけ(12/27) APIの基本設計(IRefAction/IRefFunc) // 一例 : NativeArray<long> array internal readonly struct

    LessThan : IRefFunc<long, bool> { private readonly long comparer; public LessThan(long comparer) => this.comparer = comparer; public bool Calc(ref long value) => value < comparer; } foreach(ref var item in array.Where(new LessThan(114514L))){ item = 1919810L; }
  39. おまけ(13/27) APIの基本設計(IRefAction/IRefFunc) 前スライドのWhereについて、GC.Allocはゼロ! 全てスタック上で確保されている NativeArray<T>やNativeEnumerable<T>に対する Whereでforeachをすると元の配列の要素を変更可能 安全性を投げ捨てた仕様

  40. おまけ(14/27) C#の言語仕様(1/4) 型 ク ラ ス C#8時点で未サポート C#10まで待てとかつらい…… https://github.com/dotnet/csharplang/issues/110

  41. おまけ(15/27) C#の言語仕様(1/4) public static T<A> To<T,A> (this IEnumerable<A> xs) where

    T : <>, new(), ICollection<> { var ta = new T<A>(); foreach(var x in xs) ta.Add(x); return ta; } /// 別の部分 var data = Enumerable.Range(0, 20); var set = data.To<HashSet<>, int>(); var link = data.To<LinkedList<>, int>(); var list = data.To<List<>, int>(); 10までお預け ナンデ? ※言語仕様は変化します
  42. おまけ(16/27) C#の言語仕様(2/4) T a r g e t T y

    p e d N e w C#8で入る予定だった 入らなかった……
  43. おまけ(17/27) C#の言語仕様(2/4) これは人間が書くべきか? Target Typed Newがあれば new(enumerable,new(orderComparer,new(keySelector, comparer, descending)), alloc)

  44. おまけ(18/27) C#の言語仕様(3/4) Partial Type Inference 実装時期:Any いつ入るのか、誰もわからない

  45. おまけ(19/27) C#の言語仕様(3/4) UniNativeLinqは基本的に拡張メソッドを定義している C#は型引数を引数から全て算出できる場合のみ省略可 1つでも算出できない場合全てを明示せねばならない 泣く泣く拡張メソッドを諦めたAPI多数 UNLはPartial Type Inferenceがもし来れば完全版になれる

  46. おまけ(20/27) C#の言語仕様(4/4) 構造体はfieldをref returnできない 構造体はスタック上に置かれる フィールド参照を返すと生存期間を超えかねない 安全性の保証ができないため制限されている ref構造体ならfieldのrefをreturnできる

  47. おまけ(21/27) C#の言語仕様(4/4) C#では無理? できらぁ!

  48. おまけ(22/27) C#の言語仕様(4/4) や っ た ぜ 構造体のfieldのrefを戻せないのはC#コンパイラの制限 ならばILで書こう 書いた 動いた

    実際通常の使用法では何も問題はない イイネ?
  49. おまけ(23/27) UnityにおけるILライフ(1/4) 馴染み深いであろうコード生成技術 AssemblyBuilder DynamicMethod 実行時に動的にIL生成 IL2CPPでは動かない Roslyn 事前にC#コードを生成 Linux/MacとWinで細かい挙動の違いが辛い

  50. おまけ(24/27) UnityにおけるILライフ(2/4) Mono.Cecil Unity Package Managerから何故かインストール可能 DLLを生成したり、DLLの中身を書き換えたり Unityのビルドプロセスにフックするのもあり、 IL2CPP/Mono, Win/Mac/Linux問わず動作する

  51. おまけ(25/27) UnityにおけるILライフ(3/4) Mono.Cecilの一例:UniEnumExtension 事前コード編纂によってenum.ToString()を500倍高速化 「白魔術は初心者からは暗黒魔術に見える」 ってQiitaに書いてあったし、ILはコワクナイヨ

  52. おまけ(26/27) UnityにおけるILライフ(3/4) UnityのBurstコンパイラもMono.Cecilを利用している 安心安全高信頼の暗黒白魔術 興味のある人は「Mono.Cecil入門」で学べる

  53. おまけ(27/27) エディタ拡張 横転したラベルフィールド チェックボックスで構成された行列 PhysicsのLayer Collision Matrixで見たはず Packages/uninativelinq/UNL/ScriptableObject/Double Api/DifferentNameEnumerableDoubleApi.cs こ↑こ↓(GPLv3)に記述あり

  54. 自己紹介 RTSゲームエンジン「ヴァーレントゥーガ」愛好家 ヴァーレントゥーガ再実装プロジェクト主宰 Unity/ヴァーレントゥーガMOD作成/ILの読み書き いずれかの経験の持ち主の参加を常に求めている