Slide 1

Slide 1 text

LINQ to NativeArray roppongi.unity #5 @pCYSl5EDgo

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

IEnumerableの陥穽 LINQ IEnumerable Where(this IEnumerable collection, Func predicate) Whereなどの呼び出し毎にGC Alloc発生 ゆえにインゲームではLINQ禁止が多い ※抽象クラスIteratorを継承した WhereArrayIteratorなどが実際の戻り値の型

Slide 6

Slide 6 text

IEnumerableの陥穽 GetEnumerator IEnumerator IEnumerable.GetEnumerator(); foreachする度にGetEnumeratorの戻り値分GC Alloc必須 わたしはつらい! たえられない!

Slide 7

Slide 7 text

LINQ to NativeArray 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に出す可能性がある

Slide 8

Slide 8 text

UniNativeLinq入手先(2019/10/23時点) GitHub – Free! MITとGPLv3で配布 https://github.com/pCYSl5EDgo/UniNativeLinq-EditorExtension

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

ここが嬉しい UniNativeLinq(1/5) using文との相性問題を解決可能 従来 using(var array = new NativeArray(10, Allocator.Temp)) array[0] = 10; // using変数は書き換え不能 コンパイルエラー UniNativeLinq using(var array = new NativeArray(10, Allocator.Temp)) array.AsRefEnumerable()[0] = 10;

Slide 14

Slide 14 text

ここが嬉しいUniNativeLinq(2/5) インデクサアクセスの戻り値が参照 従来 // NativeArray array; var item = array[0]; item[0, 3] = 10f; array[0] = item; UniNativeLinq // NativeArray array; array.AsRefEnumerable()[0][0, 3] = 10f;

Slide 15

Slide 15 text

ここが嬉しいUniNativeLinq(3/5) byte[]への変換が簡単 従来 // NativeArray 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() .ToArray();

Slide 16

Slide 16 text

ここが嬉しいUniNativeLinq(4/5) 基本的に速い 30%高速化

Slide 17

Slide 17 text

ここが嬉しい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#コンパイラがゆるがば過ぎて少し心配になる……ならない?

Slide 18

Slide 18 text

詳細設定(1/3) メニュー/UniNativeLinq/Open Settings “Generate DLL”ボタンをクリックして Assets/Plugins/UNL/Settings/UniNativeLinqDll.dll上書き

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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.

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

謝辞とクレジット表記(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

Slide 26

Slide 26 text

謝辞とクレジット表記(3/3) @halfsode_様 「バ美声β」使用 https://halfsode.booth.pm/items/1177717 @baku_dreameater 「VMagic Mirror」使用 https://booth.pm/ja/items/1272298

Slide 27

Slide 27 text

おまけ(1/27) APIの基本設計(IRefEnumerable) interface IRefEnumerable : IEnumerable where T : unmanaged where TEnumerator : struct, IRefEnumerator { TEnumerator GetEnumerator(); }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

おまけ(4/27) API基本設計(IRefEnumerable) bool CanFastCount() O(1)で⾧さを得られる bool CanIndexAccess() O(1)で要素にアクセスできる ToArray/ToNativeArray/ToNativeEnumerable Any/Count/LongCount よく使うのは最初から含めた

Slide 31

Slide 31 text

おまけ(5/27) APIの基本設計(IRefEnumerator) interface IRefEnumerator : IEnumerator where T : unmanaged { ref T Current { get; } bool MoveNext(); ref T TryGetNext(out bool success); bool TryMoveNext(out T value); }

Slide 32

Slide 32 text

おまけ(6/27) APIの基本設計(IRefEnumerator) ref T Current { get; } 基本 foreach(ref var item in collection)と書くのに必須 構造体のコピーコストを避けられる

Slide 33

Slide 33 text

おまけ(7/27) APIの基本設計(IRefEnumerator) bool TryMoveNext(out T value); interface越しに呼び出しをすると MoveNextメソッド/Currentプロパティと2回呼び出す 仮想関数テーブルを2回ルックアップするのは無駄 では1回にまとめようと目的の下に生まれたAPI

Slide 34

Slide 34 text

おまけ(8/27) APIの基本設計(IRefEnumerator) ref T TryGetNext(out bool success) TryMoveNextはout引数valueに必ず書き込む 無駄なコピーコスト→ref Tならば参照を返すだけ 巨大構造体(Matrix4x4等)において TryGetNextの方が性能的に優秀(✗書き心地)

Slide 35

Slide 35 text

おまけ(9/27) APIの基本設計(IRefAction/IRefFunc) 一例 public struct SelectEnumerable : IRefEnumerable.Enumerator, T>, IEnumerable, IEnumerable where TPrevEnumerable : struct, IRefEnumerable where TPrevEnumerator : struct, IRefEnumerator where TPrev : unmanaged where T : unmanaged where TAction : struct, IRefAction 型と制約が⾧い!⾧過ぎる!

Slide 36

Slide 36 text

おまけ(10/27) APIの基本設計(IRefAction/IRefFunc) SelectEnumerable`5の注目点は where TAction where struct, IRefAction interface IRefAction{void Execute(ref T0 arg0,ref T1 arg1)} 構造体の型に処理内容を紐付ける

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

おまけ(12/27) APIの基本設計(IRefAction/IRefFunc) // 一例 : NativeArray array internal readonly struct LessThan : IRefFunc { 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; }

Slide 39

Slide 39 text

おまけ(13/27) APIの基本設計(IRefAction/IRefFunc) 前スライドのWhereについて、GC.Allocはゼロ! 全てスタック上で確保されている NativeArrayやNativeEnumerableに対する Whereでforeachをすると元の配列の要素を変更可能 安全性を投げ捨てた仕様

Slide 40

Slide 40 text

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

Slide 42

Slide 42 text

おまけ(16/27) C#の言語仕様(2/4) T a r g e t T y p e d N e w C#8で入る予定だった 入らなかった……

Slide 43

Slide 43 text

おまけ(17/27) C#の言語仕様(2/4) これは人間が書くべきか? Target Typed Newがあれば new(enumerable,new(orderComparer,new(keySelector, comparer, descending)), alloc)

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

おまけ(22/27) C#の言語仕様(4/4) や っ た ぜ 構造体のfieldのrefを戻せないのはC#コンパイラの制限 ならばILで書こう 書いた 動いた 実際通常の使用法では何も問題はない イイネ?

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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