20210425のC#に関する記事は4件です。

オブジェクトに複数のリンクを張って縦横無尽に操作するライブラリ CrossLink

CrossLink ソースジェネレーターと Arc.Collection を使用したC#ライブラリです。 オブジェクト間に複数のリンクを張って、柔軟に管理したり検索したり出来ます。 よく分からない? オブジェクトT に対して、カスタムList<T> を作成します。しかも、普通のジェネリックコレクションより柔軟で拡張性があり、なおかつ高速です。 一言で言えば、速くて便利!オブジェクトを扱うプログラムでは必須です! ええ、こんな説明じゃ分からないでしょう。 下のサンプルコードをみてください。 Table of Contents Quick Start Performance How it works Chains Features Serialization AutoNotify AutoLink ObservableCollection Quick Start ソースジェネレーターなので、ターゲットフレームワークは .NET 5 以降です。 まずはPackage Manager Consoleでインストール。 Install-Package CrossLink サンプルコードです。 using System; using System.Collections.Generic; using CrossLink; #pragma warning disable SA1300 namespace ConsoleApp1 { [CrossLinkObject] // 対象のクラスに CrossLinkObject属性を追加します public partial class TestClass // ソースジェネレーターでコード追加するので、partial classが必須 { [Link(Type = ChainType.Ordered)] // 対象のメンバーにLink属性を追加します。TypeにChainType(Collectionの種類のようなもの)を指定します。 private int id; // 対象となるメンバー。これを元に、プロパティ Id と IdLink が追加されます。 // プロパティ Id を使用して、値の取得・更新(値、リンク)を行います。 // プロパティ IdLink はオブジェクト間の情報を保存します。CollectionのNodeのようなものです。 [Link(Type = ChainType.Ordered)] // ChainType.Ordered はソート済みコレクション。SortedDictionary と考えていただけば public string name { get; private set; } = string.Empty; // プロパティ Name と NameLink が追加 [Link(Type = ChainType.Ordered)]// 同上 private int age; // プロパティ Age と AgeLink が追加 [Link(Type = ChainType.StackList, Name = "Stack")] // Nameで名称を指定して、StackListを追加。コンストラクターには複数のLinkを付加出来ます。 [Link(Type = ChainType.List, Name = "List")] // Listを追加 public TestClass(int id, string name, int age) { this.id = id; this.name = name; this.age = age; } public override string ToString() => $"ID:{this.id,2}, {this.name,-5}, {this.age,2}"; } public class Program { public static void Main(string[] args) { Console.WriteLine("CrossLink Quick Start."); Console.WriteLine(); var g = new TestClass.GoshujinClass(); // まずは、オブジェクト管理のクラス Goshujin を作成 new TestClass(1, "Hoge", 27).Goshujin = g; // TestClassを作成し、Goshujinを設定します。Goshujin側にもTestClassが登録されます。 new TestClass(2, "Fuga", 15).Goshujin = g; new TestClass(1, "A", 7).Goshujin = g; new TestClass(0, "Zero", 50).Goshujin = g; ConsoleWriteIEnumerable("[List]", g.ListChain); // ListChain(コンストラクタにLinkが追加されたやつ)は実質的に List<TestClass> です /* Result; 作成順に並びます ID: 1, Hoge , 27 ID: 2, Fuga , 15 ID: 1, A , 7 ID: 0, Zero , 50 */ Console.WriteLine("ListChain[2] : "); // インデックスアクセスが可能 Console.WriteLine(g.ListChain[2]); // ID: 1, A , 7 Console.WriteLine(); ConsoleWriteIEnumerable("[Sorted by Id]", g.IdChain); /* IdChain は ChainType.Ordered なので、ソート済み ID: 0, Zero , 50 ID: 1, Hoge , 27 ID: 1, A , 7 ID: 2, Fuga , 15 */ ConsoleWriteIEnumerable("[Sorted by Name]", g.NameChain); /* 同様にNameでソート済み ID: 1, A , 7 ID: 2, Fuga , 15 ID: 1, Hoge , 27 ID: 0, Zero , 50 */ ConsoleWriteIEnumerable("[Sorted by Age]", g.AgeChain); /* 同様にAgeでソート済み ID: 1, A , 7 ID: 2, Fuga , 15 ID: 1, Hoge , 27 ID: 0, Zero , 50 */ var t = g.ListChain[1]; Console.WriteLine($"{t.Name} age {t.Age} => 95"); t.Age = 95; // Fugaの年齢を95にすると、 ConsoleWriteIEnumerable("[Sorted by Age]", g.AgeChain); /* なんと AgeChain が更新されています! ID: 1, A , 7 ID: 1, Hoge , 27 ID: 0, Zero , 50 ID: 2, Fuga , 95 */ ConsoleWriteIEnumerable("[Stack]", g.StackChain); /* こちらは Stack ID: 1, Hoge , 27 ID: 2, Fuga , 95 ID: 1, A , 7 ID: 0, Zero , 50 */ t = g.StackChain.Pop(); // Stackの先頭のオブジェクトを取得し、Stackから削除します。影響するのはStackChainだけなのでご注意ください。 Console.WriteLine($"{t.Name} => Pop"); t.Goshujin = null; // 他のChainから削除するには、Goshujinをnullにします。 Console.WriteLine(); ConsoleWriteIEnumerable("[Stack]", g.StackChain); /* Zero が解放されました・・・ ID: 1, Hoge , 27 ID: 2, Fuga , 95 ID: 1, A , 7 */ var g2 = new TestClass.GoshujinClass(); // Goshujin2 を作成 t = g.ListChain[0]; Console.WriteLine($"{t.Name} Goshujin => Goshujin2"); Console.WriteLine(); t.Goshujin = g2; // Goshujin から Goshujin2 に変更すると ConsoleWriteIEnumerable("[Goshujin]", g.ListChain); ConsoleWriteIEnumerable("[Goshujin2]", g2.ListChain); /* 各種Chainが更新されます * [Goshujin] ID: 2, Fuga , 95 ID: 1, A , 7 [Goshujin2] ID: 1, Hoge , 27*/ // g.IdChain.Remove(t); // t は Goshujin2 の所有物なので、これはエラー // t.Goshujin.IdChain.Remove(t); // こちらはOK(t.GosjujinはGoshujin2) Console.WriteLine("[IdChain First/Next]"); t = g.IdChain.First; // Link interfaceを使って、オブジェクトを列挙します while (t != null) { Console.WriteLine(t); t = t.IdLink.Next; // Nextの型はLinkではなく、Objectそのものなのでご注意ください } static void ConsoleWriteIEnumerable<T>(string? header, IEnumerable<T> e) {// オブジェクトを画面に出力 if (header != null) { Console.WriteLine(header); } foreach (var x in e) { Console.WriteLine(x!.ToString()); } Console.WriteLine(); } } } } Performance パフォーマンスは最優先事項です。 CrossLinkは、ジェネリックコレクションより込み入った処置を行っていますが、実際はジェネリックコレクションより高速に動作します(主にArc.Collectionのおかげです)。 SortedDictionary<TKey, TValue> と比べてみましょう。 H2HClass という簡単なクラスを作成します。 [CrossLinkObject] public partial class H2HClass2 { public H2HClass2(int id) { this.id = id; } [Link(Type = ChainType.Ordered)] private int id; } ジェネリック版。クラスを作成し、コレクションに追加していきます。 var g = new SortedDictionary<int, H2HClass>(); foreach (var x in this.IntArray) { g.Add(x, new H2HClass(x)); } こちらはCrossLink版。同じような処理をしています。 var g = new H2HClass2.GoshujinClass(); foreach (var x in this.IntArray) { new H2HClass2(x).Goshujin = g; } こちらが結果。 なんとSortedDictionary<TKey, TValue> より高速です。 Method Length Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated NewAndAdd_SortedDictionary 100 7,209.8 ns 53.98 ns 77.42 ns 1.9379 - - 8112 B NewAndAdd_CrossLink 100 4,942.6 ns 12.28 ns 17.99 ns 2.7084 0.0076 - 11328 B Id を変更すると、当然コレクションの更新(値の削除・追加)が必要です。 CrossLinkは断然高速で、SortedDictionary の約3倍のパフォーマンスです(CrossLinkは内部でNodeを保持しているので、当然と言えば当然ですが)。 Method Length Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated RemoveAndAdd_SortedDictionary 100 1,491.1 ns 13.01 ns 18.24 ns 0.1335 - - 560 B RemoveAndAdd_CrossLink 100 524.1 ns 3.76 ns 5.63 ns 0.1717 - - 720 B How it works CrossLinkは既存のクラスに、GoshujinClassという内部クラスと、いくつかのプロパティを追加することで動作します。 実際には、 GoshujinClass という内部クラスを追加 Goshujin プロパティを追加 Link 属性が付加されたメンバーに対応するプロパティを追加します。プロパティ名は、メンバー名の頭文字が大文字に変換されたものです(id なら Id になる)。 Link 属性が付加されたメンバーに対応するLink フィールドを追加します。こちらの名称は、プロパティ名にLinkがついたものになります(Id なら IdLink になる)。 という流れです。 用語 Object: 情報を保持する、一般的なオブジェクト。 Goshujin: オブジェクトのオーナークラス。このクラスを介して、オブジェクトの管理・操作を行います。 Chain: コレクションのようなもの。Goshujin は複数の Chain を保持し、オブジェクトを様々な形式で管理できます。 Link: コレクションにおけるNodeのようなもの。オブジェクトは内部に複数のLinkを持ち、オブジェクト間の情報を保持します。 実際に、ソースジェネレーターでどのようなコードが生成され、どのようにCrossLinkが動作するのか見てみましょう。 まずは TinyClass という非常にシンプルなクラスを作成します。メンバーは id 一つだけです。 public partial class TinyClass // partial class が必須 { [Link(Type = ChainType.Ordered)] // Link属性を追加 private int id; } プロジェクトをビルドすると、CrossLinkはまず GoshujinClassという内部クラスを作成します。GoshujinClass は TinyClass を操作・管理するクラスです。 public sealed class GoshujinClass : IGoshujin {// ご主人様は、日本語で Goshujin-sama という意味です public GoshujinClass() { // IdChainはTinyClassのソート済みコレクションです this.IdChain = new(this, static x => x.__gen_cl_identifier__001, static x => ref x.IdLink); } public OrderedChain<int, TinyClass> IdChain { get; } // 内部では Arc.Collection のコレクションクラスを使用しています } 次のコードでは Goshujin インスタンス/プロパティを追加します。 private GoshujinClass? __gen_cl_identifier__001; // 実際の Goshujinインスタンス public GoshujinClass? Goshujin { get => this.__gen_cl_identifier__001; set {// Goshujinインスタンスをセットします if (value != this.__gen_cl_identifier__001) { if (this.__gen_cl_identifier__001 != null) {// TinyClassを以前のGoshujinから解放します this.__gen_cl_identifier__001.IdChain.Remove(this); } this.__gen_cl_identifier__001 = value;// インスタンスを設定します if (value != null) {// 新しいGoshujinにお仕えします value.IdChain.Add(this.id, this); } } } } 最後に、メンバーに対応する Link と プロパティを追加します。 inally, CrossLink adds a link and a property which is used to modify the collection and change the value. public OrderedChain<int, TinyClass>.Link IdLink; // Link is like a Node. public int Id {// プロパティ "Id" は、メンバー "id" から作成されました get => this.id; set { if (value != this.id) { this.id = value; // 値が更新されると、IdChainも更新されます this.Goshujin.IdChain.Add(this.id, this); } } } Chains Chainはオブジェクトのコレクションクラスのようなもので、CrossLinkでは以下のChainを実装しています。 Name Structure Access Add Remove Search Sort Enum. ListChain Array Index O(1) O(n) O(n) O(n log n) O(1) LinkedListChain Linked list Node O(1) O(1) O(n) O(n log n) O(1) QueueListChain Linked list Node O(1) O(1) O(n) O(n log n) O(1) StackListChain Linked list Node O(1) O(1) O(n) O(n log n) O(1) OrderedChain RB Tree Node O(log n) O(log n) O(log n) Sorted O(log n) ReverseOrderedChain RB Tree Node O(log n) O(log n) O(log n) Sorted O(log n) UnorderedChain Hash table Node O(1) O(1) O(1) - O(1) ObservableChain Array Index O(1) O(n) O(n) O(n log n) O(1) こーゆーChainが欲しい的な要望ありましたらご連絡ください。 Features Serialization 複雑にリンクされたオブジェクトのシリアライズは結構面倒です。 しかし、Tinyhand との合わせ技で簡単にシリアライズできます! やり方は簡単。Tinyhand パッケージをインストールして、TinyhandObject 属性を追加して、Key 属性を各メンバーに追加するだけです! Install-Package Tinyhand [CrossLinkObject] [TinyhandObject] // TinyhandObject属性を追加 public partial class SerializeClass // partial class を忘れずに { [Link(Type = ChainType.Ordered, Primary = true)] // Primary Link(すべてのオブジェクトが登録されるLink)を指定すると、さらにシリアライズのパフォーマンスが向上します [Key(0)] // Key属性(シリアライズの識別子。stringかint)を追加 private int id; [Link(Type = ChainType.Ordered)] [Key(1)] private string name = default!; public SerializeClass() {// Tinyhandのデシリアライズ処理のため、デフォルトコンストラクタ(引数のないコンストラクタ)が必要です } public SerializeClass(int id, string name) { this.id = id; this.name = name; } } テストコード: var g = new SerializeClass.GoshujinClass(); // Goshujinを作成 new SerializeClass(1, "Hoge").Goshujin = g; // オブジェクト追加 new SerializeClass(2, "Fuga").Goshujin = g; var st = TinyhandSerializer.SerializeToString(g); // これだけでシリアライズ出来ます! var g2 = TinyhandSerializer.Deserialize<SerializeClass.GoshujinClass>(TinyhandSerializer.Serialize(g)); // バイナリにシリアライズして、それをデシリアライズします。簡単でしょう? AutoNotify Link 属性の AutoNotifyプロパティを true にすると、CrossLinkは INotifyPropertyChanged を自動で実装します。 [CrossLinkObject] public partial class AutoNotifyClass { [Link(AutoNotify = true)] // AutoNotifyをtrueに private int id; public void Reset() { this.SetProperty(ref this.id, 0); // SetPropertyを呼ぶと、手動で値の更新とPropertyChanged の呼び出しが出来ます。 } } テストコード: var c = new AutoNotifyClass(); c.PropertyChanged += (s, e) => { Console.WriteLine($"Id changed: {((AutoNotifyClass)s!).Id}"); }; c.Id = 1; // 値を変更すると、自動的に PropertyChange が呼ばれます。 c.Reset(); // 手動で 生成コード: public partial class AutoNotifyClass : System.ComponentModel.INotifyPropertyChanged { public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged; protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer<T>.Default.Equals(storage, value)) { return false; } storage = value; this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); return true; } public int Id { get => this.id; set { if (value != this.id) { this.id = value; this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs("Id")); } } } } AutoLink デフォルトの動作では、オブジェクトのGoshujinが設定されると自動でオブジェクトをリンク(GoshujinのChainに登録する)します。 自動でリンクしたくない場合は、AutoLink プロパティを false に設定してください。 [CrossLinkObject] public partial class ManualLinkClass { [Link(Type = ChainType.Ordered, AutoLink = false)] // AutoLinkをfalse private int id; public ManualLinkClass(int id) { this.id = id; } public static void Test() { var g = new ManualLinkClass.GoshujinClass(); var c = new ManualLinkClass(1); c.Goshujin = g; // 自動でリンクされません Debug.Assert(g.IdChain.Count == 0, "Chain is empty."); g.IdChain.Add(c.id, c); // 手動でリンクします Debug.Assert(g.IdChain.Count == 1, "Object is linked."); } } ObservableCollection MVVM?バインディング? 面倒なことばかりでしょう。 ObservableChain を使うと、簡単にバインディングできます。 コンストラクタに [Link(Type = ChainType.Observable, Name = "Observable")] を追加するだけです。 [CrossLinkObject] public partial class ObservableClass { [Link(Type = ChainType.Ordered, AutoNotify = true)] // もちAutoNotify private int id { get; set; } [Link(Type = ChainType.Observable, Name = "Observable")] public ObservableClass(int id) { this.id = id; } } テストコード: var g = new ObservableClass.GoshujinClass(); ListView.ItemSource = g.ObservableChain;// ObservableChainをObservableCollectionのように使用できます new ObservableClass(1).Goshujin = g;// これでListViewが更新!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity初心者のメモ帳

what's this これはunity,C#の初心者が開発を進めるにあたってメモしたほうがいいなということをメモしていきます.誰かの参考になればと公開記事にしておきます.随時更新していきます. 目次 基本操作編 エラー編 NullReferenceException
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テキストコンバーター

UnityのJsonUtilityではDictionaryをJson化することができないので、自分で作ったテキストデータをDictionaryに変換するスクリプトを組んでみました。 #で始まる行はコメント :で終わる行はキー <>で囲まれた範囲を値 としてDictionaryにします。 と組み合わせることで、セリフや説明を多言語に対応させることを目的にしたものです。 MessageControl using System.Collections; using System.Collections.Generic; using System.Text.RegularExpressions; using UnityEngine; public class MessageControl { private string MessageData = ""; private Regex SearchTag = new Regex(@"^[^#].+?:", RegexOptions.Multiline); private Regex BracketRange = new Regex(@"<[\s\S]*?>", RegexOptions.Multiline); private Regex BracketSelector = new Regex(@"<|>", RegexOptions.Multiline); public MessageControl(string strmes) { MessageData = strmes; } public Dictionary<string, string> ConvertDictionary() { Dictionary<string, string> rDic = new Dictionary<string, string>(); MatchCollection matches = SearchTag.Matches(MessageData); bool hasContent = false; for (int i = 0; i < matches.Count; i++) { Match match = BracketRange.Match(MessageData, matches[i].Index); if (match.Success) { string key = matches[i].Value.ToUpper(); string value = BracketSelector.Replace(match.Value, "").Replace("\r", "").Replace("\n", ""); rDic.Add(key, value); hasContent = true; } } if (!hasContent) { rDic = null; Debug.Log("Dictionary Setup Failed."); } return rDic; } } テキストデータの例 # #メッセージデータ日本語JP/英語EN # TUTORIAL00JP: < [ANIM20]それじゃ[RET]画面の見方とゲームのルール、操作方法について説明しよう[CLICK][NEXT] > TUTORIAL00EN: < [ANIM20]OK Then,[RET]I will explain about the rule of the game,and controls[CLICK][NEXT] > TUTORIAL01JP: < [PIC0][LOOK][ANIM21]まずはタイトル画面、[RET]ここでは各種エンディングを何人見たかということがわかる[CLICK] [OUT]設定ボタンを押すと設定画面になり、GAMESTARTを押すとゲームがスタートする[CLICK] [ANIM25]ま、そのくらいのことはわかってると思うけどね[CLICK][NEXT] > TUTORIAL01EN: < [PIC0][LOOK][ANIM21]First,let's take a look at this title screen.[CLICK] The informations displaying how many peoples have looked each endings.[CLICK] [OUT]The Option button is for opening to display setting screen,the GameStart button is for starting game[CLICK] [ANIM25]You know about that, don't you?[CLICK][NEXT] > TUTORIAL02JP: < [PIC1][LOOK][ANIM21]これは設定画面だ[RET]BGM、効果音の音量設定、[OUT]言語は英語と日本語から選べる[CLICK] さらにパーティクルを表示するかどうか設定でき、最後のオプションは条件を満たすと選択できるようになる[CLICK] [ANIM25]最後のオプションの効果については今はまだヒミツだ[PREL500][CLICK][NEXT] > TUTORIAL02EN: < [PIC1][LOOK][ANIM21]This is setting screen.You can change the volume of BGM or SE on this.[CLICK] [OUT]And also you can choose English or Japanese languages.[CLICK] [ANIM25]Bottom option which named "Guardian" is secret yet.[PREL500][CLICK][NEXT] > TUTORIAL03JP: < [PIC2][ANIM25]次はゲーム画面について説明しよう[CLICK] [PIC3][ANIM21]上部の長いバーがテンションゲージで、左側の丸いゲージがライフゲージだ[CLICK] [ANIM25]テンションゲージは正しい場所を正しい順番でクリックすると増加する[CLICK] 前作とは違い、テンションゲージがMAXになることでエンディングに分岐する[CLICK] [ANIM21]ライフゲージはクリックすることで減っていき、0になるとゲームオーバーだ[CLICK] [ANIM25]ただしアイコンをクリックした場合はHPは減らないので安心してほしい[CLICK][NEXT] > TUTORIAL03EN: < [PIC2][ANIM25]Next,this is game playing screen.[CLICK] [PIC3][ANIM21]Horizontal bar is progress of tension,circle gauge is your hitpoint.[CLICK] [ANIM25]If you click correct place in correct order,tenstion bar will increase.[CLICK] When tensition bar filled,Ending movie will be started. [CLICK] [ANIM21]Though clicking on it will decrease your hit point.[CLICK] If you lose all your hit points, game is over be careful.[CLICK][NEXT] > TUTORIAL04JP: < [PIC5][ANIM21]正しい部分をクリックすると左側の画面のようにハートマークが舞い、テンションゲージが増加する[CLICK] [ANIM25]だが、ボクの視界内でクリックしたら右側の画面のように問答無用で凍らせてもらうぞ[CLICK] [ANIM22]凍るとHPに大ダメージを受けて少しの間動けなくなり、ゲームオーバーに近づく[CLICK] [ANIM25]まあ、せいぜい気をつけることだね[CLICK][NEXT] > TUTORIAL04EN: < [PIC5][ANIM21]When you click on the correct place, the heart particle will dance at click point[CLICK] and then the tension bar will increase.[CLICK] [ANIM25]But if you click on in my sight, you will be frozen and will be taken damage.[CLICK][NEXT] > TUTORIAL05JP: < [ANIM21][PIC4]ゲーム画面のアイコンは左側がズームとオプション、右側は移動とカメラリセットだ[CLICK] [ANIM25]左下の下向きの矢印ボタンをクリックすると全てのアイコンが消えた状態になり、[CLICK] もう一度クリックすれば再びアイコンが表示される[CLICK][NEXT] > TUTORIAL05EN: < [ANIM21][PIC4]The User Interface icons on the screen are zoom and option buttons on the left,[CLICK] movement and camera reset buttons on the right.[CLICK] [ANIM25]Button with down arrow will hide all icons from screen.[CLICK][NEXT] > TUTORIAL06JP: < [PIC6][ANIM21]最後に説明するのはこのファンティアボタン[CLICK] クリックするとファンティアのサークルページにジャンプできる[CLICK] [ANIM25]ファンティアのサークルページではこのゲームの攻略動画を公開しているぞ[CLICK] [ANIM26][PREL500]どうしてもエンディングの答えが知りたい場合は訪問してくれ[CLICK] [ANIM25]説明はだいたい以上だ[CLICK][NEXT] > TUTORIAL06EN: < [PIC6][ANIM21]The last thing I'll explain is about this Fantia button.[CLICK] This button navigate you to my circle page in Fantia by clicking.[CLICK] [ANIM25]This game's walkthrough movie are available at my page in Fantia with plan named "nostalgy plan" .[CLICK] [ANIM22]This is the end of the explanation.[CLICK][NEXT] >
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity] C# JobSystem + Burst でテキストファイルを非同期に高速パース

以前の記事: [Unity] C# JobSystem を利用してテキストファイルを非同期でパースする で、Unityで外部のテキストファイルを非同期かつそこそこの速度でパースすることを実現しました。 しかし、その結びに課題の一つとして以下の点を挙げていました。 ◎Burstでもっと速くならない? いつになるかは不明ですが、公式の案内では char には対応する予定らしいので、その暁にはもっと早くなるはず。 Burst does not support the following types: - char (this will be supported in a future release) - string as this is a managed type ライブラリ内部ではASCII範囲の値しか検索、比較していないので、 char をすべて unit16 あたりにキャストして、関数ポインタ経由でBurstさせればもっと早くなる可能性は大いにあります。 しかし、公式が対応すると明言していますし、上記の手法で Burst による高速化が特に期待されるホットスポットは TryParse() や Split() 関数なので、Burst が char に対応したならユーザーデータクラスの ParseLine(line) をまるごと適用したほうがはるかに効果的でしょう。 本記事は「キャストするだけなら機械的に書き換えればいいし楽そうだからやってみよう」と、適当に手を出してハマった箇所のレポートと、その成果物の紹介です。 成果物 前回に続き、今回の記事の内容をアップデートしたものを公開しています。 GitHub NativeStringCollections 基本的な利用方法やライブラリの大まかな設計については前回の記事をご参照ください。 動作環境 Unity 2019.4.24f1 Collections 0.9.0-preview.6 Burst 1.4.7 Burst 版の性能評価 検証環境: parts OS Windows 10 CPU Ryzen5 3600X GPU GTX 1070 Storage NVMe SSD (PCIe Gen3 x4) 50万Charaのテキストデータ(ノイズ、追加データあり。約59万行。デモシーン付属のジェネレータで生成)のパース時間を計測した。 AsyncTextFileReader<T> の測定シーンは Assets/NativeStringCollections/Samples/Demo/Scenes/Demo_ReadingLargeFile.unity を、 C#の標準機能のみを用いた場合の測定には Assets/NativeStringCollections/Samples/Demo/Scenes/Demo_StandardCSharp_(comparison).unity 使用した。 条件 Time [ms] 備考 C# standard (参考値) 710 ~ 850 C# の標準機能のみ利用 C# IL2CPP (Burstなし) 500 ~ 550 ParseLinesWorker (改行コード解析) をBurst 460 ~ 480 ライブラリ内部の関数なのでデフォルトで有効 Burst適用例(1) 小規模関数を個別に: Split(), TryParse() 430 ~ 450 NativeStringCollections.BurstFunc にデリゲートを定義済み Burst適用例(2) 大きな処理単位で : ParseLines(), PostReadProc() 全体 240 ~ 250 対象のファイルに合わせてユーザが実装 次に、先と同様の50万Charaのテキストデータを複数用意し、AsyncTextFileLoader<T>を用いた並列ファイルパース実行時の性能を、(1):ParseLines(), PostReadProc()全体へBurstを適用した場合, (2):ユーザ側でのBurst適用を行わない場合の2通りについて計測した。 測定シーンには Assets/NativeStringCollections/Samples/Demo/Scenes/Demo_AsyncMultiFileManagement.unity を使用した。 並列Job数 (1) Time (Burst) [ms] (2) Time [ms] 1 240 ~ 250 460 ~ 480 2 245 ~ 270 460 ~ 500 3 245 ~ 275 470 ~ 540 4 255 ~ 290 500 ~ 580 6 270 ~ 350 550 ~ 650 8 300 ~ 390 600 ~ 700 上記の結果から、NativeStringCollections を使用した場合はユーザーが特別な実装をしなくてもファイル読み込み速度が約1.5倍ほど高速になります。 さらに、ユーザーがBurstを適用したITextFileParser.ParseLines()を実装した場合、C#標準と比較し、最高で3倍近く速くなります。 また、およそCPUの物理コア数以下のJob数であればほとんど性能低下はなく、同時実行Job数の上限はAsyncTextFileLoader.MaxJobCountプロパティにより容易に制御できます。 処理はネイティブメモリ上で行われるので、大規模なテキストを処理してもマネージドメモリを圧迫せずGCを起こしません。 上記の並列Jobの性能計測では1つあたり約37MBのテキストファイル24個、合計約870MBを処理しています。 次項で説明しますが、手間になる部分はNativeStringCollections側であらかじめ実装してありますので、利用時にはユーザー側でITextFileParser.ParseLines()のBurst版を実装することを推奨いたします。 ユーザー定義パーサーでのBurstの使い方 ▽ITextFileParser.ParseLines()にBurstを適用 (推奨) ITextFileParser.ParseLines() にBurstを適用する例 using Unity.Burst; using NativeStringCollections; using NativeStringCollections.Utility; public struct YourDataElem { public ReadOnlyStringEntity text; public int somethingData; } public class YourDataParser : ITextFileParser, IDisposable { // 処理したいデータの実体 public NativeList<YourDataElem> data_list; private NativeStringList text_field_list; // ReadOnlyStringEntity.Split() の一時結果格納用 private NativeList<ReadOnlyStringEntity> str_list; private bool _allocated; // Burst に処理対象を渡すためのデータパック // ref struct で渡す必要があるのでまとめておくと楽 public struct DataPack { // コンテナに対する UnsafeReference (NativeContainer はそのままでは渡せない) // NativeStringCollections.Utility に定義済みの UnsafeRefTo**** を利用するか、 // Unity.Collections.LowLevel.Unsafe にある Unsafe なコンテナを使用、 // またはそれらをもとに自作する public UnsafeRefToNativeList<YourDataElem> data_list; public UnsafeRefToNativeStringList text_field_list; public UnsafeRefToNativeList<ReadOnlyStringEntity> str_list; } private DataPack pack; public void Init() { data_list = new NativeList<YourDataElem>(Allocator.Persistent); text_field_list = new NativeStringList(Allocator.Persistent); str_list = new NativeList<ReadOnlyStringEntity>(Allocator.Persistent); // DataPack に UnsafeRef を作成 // NativeStringCollections.Utility に定義済みの支援関数を利用するか、 // Unity.Collections.LowLevel.Unsafe にある Unsafe なコンテナを使用、 // またはそれらをもとに自作する pack.data_list = data_list.GetUnsafeRef(); pack.text_field_list = text_field_list.GetUnsafeRef(); pack.str_list = str_list.GetUnsafeRef(); _allocated = true; } public void Clear() { data_list.Clear(); text_field_list.Clear(); } public bool ParseLines(NativeStringList lines) { // Burst でコンパイルされた関数の呼び出し var input_lines = lines.GetUnsafeRef(); return YourDataParserBurst.ParseLines(ref pack, ref input_lines); } public void PostReadProc() { // Burst 実装例は割愛 ParseLines() と同様に // NativeStringList にテキストを保存する場合は // StringEntity の取り出しはデータの追加が終わったこのタイミングで行う // (内部バッファの再確保でそれまで生成した StringEntity が無効な参照になる) } public void UnLoad() { // 略 } public void Dispose() { if (_allocated) { data_list.Dispose(); text_field_list.Dispose(); str_list.Dispose(); _allocated = false; } } ~YourDataParser() { this.Dispose(); GC.SuppressFinalize(this); } } // Burst でコンパイルされる関数を実装する static class [BurstCompile] public static class YourDataParserBurst { private delegate void ParseLinesDelegate(ref YourDataParser.DataPack pack, ref UnsafeRefToNativeStringList lines, out bool continueRead); private static ParseLinesDelegate _parseLinesDelegate; // Player 開始時にBurstでコンパイルしdelegateに格納 [RuntimeInitializeOnLoadMethod] public static void Initialize() { _parseLinesDelegate = BurstCompiler. CompileFunctionPointer<ParseLinesDelegate>(ParseLinesBurstEntry).Invoke; } // Burst でコンパイルされた関数を呼ぶためのインターフェイス // 普通の C# 関数でラップすれば ref 渡しや void にこだわる必要はない public static bool ParseLines(ref YourDataParser.DataPack pack, ref UnsafeRefToNativeStringList lines) { _parseLinesDelegate(ref pack, ref lines, out bool continueRead); return continueRead; } // Burst でコンパイルされる関数の実装 // 返値は void, // 引数は ref struct, struct*, out struct のいずれか // (Burst <-> C# script の境界の制約) [BurstCompile] [AOT.MonoPInvokeCallback(typeof(ParseLinesDelegate))] private static void ParseLinesBurstEntry(ref YourDataParser.DataPack pack, ref UnsafeRefToNativeStringList lines, out bool continueRead) { continueRead = true; for(int i=0; i<lines.Length; i++) { continueRead = ParseLineImpl(ref pack, lines[i]); } } // Burst でコンパイルされる関数 の中で呼ばれる関数は // 参照型さえ使わなければ普通の書き方でよい private static bool ParseLineImpl(ref YourDataParser.DataPack pack, ReadOnlyStringEntity line) { if (line.Length < 1) return true; // 空行 line.Split(',', pack.str_list); // CSVとして ',' 区切りで分割 // 以下は "ElemName,IntValue" な CSV の場合の実装例 pack.text_field_list.Add(pack.str_list[0]); var elem = new YourDataElem(); pack.str_list[1].TryParse(out elem.somethingData); pack.data_list.Add(elem); return true; } } Burst のコンパイル対象にするために static class に関数の実装とコンパイル結果を保持するdelegateを定義する必要がありますが、NativeStringCollectionsが提供しているNativeStringListとStringEntityおよびその補助関数類を利用する限り特に変わった実装をする必要はありません。 NativeContiner はスレッドセーフの実現のために内部に参照型を持っており、Burstでコンパイルする領域では参照型は使えないのでBurstが扱える型に変換しないと渡せません。技術的な詳細は後述しますが、NativeStringCollections では以下のユーティリティーを提供しています。 using NativeStringCollections; using NativeStringCollections.Utility; // for container UnsafeRefToNativeList<T> ref_to_native_list = NativeList<T>.GetUnsafeRef(); UnsafeRefToNativeStringList ref_to_native_string_list = NativeStringList.GetUnsafeRef(); UnsafeRefToNativeJaggedList<T> ref_to_native_jagged_list = NativeJaggedList<T>.GetUnsafeRef(); // for Base64 converter UnsafeRefToNativeBase64Encoder ref_to_base64_encoder = NativeBase64Encoder.GetUnsafeRef(); UnsafeRefToNativeBase64Decoder ref_to_base64_decoder = NativeBase64Decoder.GetUnsafeRef(); これらの unsafe な reference は各コンテナおよびコンバータの内部データへの参照を取得するため、UnsafeRefTo**** への変更は本体へ反映されます。参照の生成も GetUnsafeRef() により簡易に所得できますので、利用にあたって特に難しいことはないと思います。 こちらの実装法をとるか否かでパース速度がおよそ2倍違うので、多少の手間はかかるもののユーザー自身でBurstを適用したParseLines()を実装することを推奨します。 ▽NativeStringCollections.BurstFunc (定義済み delegate) の利用 NativeStringCollections.BurstFunc を利用する例 using NativeStringCollections; using NativeStringCollections.Utility; public struct YourDataElem { public ReadOnlyStringEntity text; public int somethingData; } public class YourDataParser : ITextFileParser, IDisposable { // 処理したいデータの実体 public NativeList<YourDataElem> data_list; private NativeStringList text_field_list; // ReadOnlyStringEntity.Split() の一時結果格納用 private NativeList<ReadOnlyStringEntity> str_list; private bool _allocated; public void Init() { data_list = new NativeList<YourDataElem>(Allocator.Persistent); text_field_list = new NativeStringList(Allocator.Persistent); str_list = new NativeList<ReadOnlyStringEntity>(Allocator.Persistent); _allocated = true; } public void Clear() { data_list.Clear(); text_field_list.Clear(); } public bool ParseLines(NativeStringList lines) { bool continueRead = true; for(int i=0; i<lines.Length; i++) { continueRead = ParseLineImpl(lines[i]); if (!continueRead) return false; } return true; } private bool ParseLineImpl(ReadOnlyStringEntity line) { if (line.Length < 1) return true; // 空行 line.Split(',', str_list); // CSVとして ',' 区切りで分割 // 以下は "ElemName,IntValue" な CSV の場合の実装例 text_field_list.Add(str_list[0]); var elem = new YourDataElem(); BurstFunc.TryParse(str_list[1], out elem.somethingData); // Burst 適用済み delegate data_list.Add(elem); return true; } public void PostReadProc() { // 略 } public void UnLoad() { // 略 } public void Dispose() { if (_allocated) { data_list.Dispose(); text_field_list.Dispose(); str_list.Dispose(); _allocated = false; } } ~YourDataParser() { this.Dispose(); GC.SuppressFinalize(this); } } あらかじめ以下の関数を NativeStringCollections.BurstFunc に定義してありますので、これを利用します。 Split() Strip(), Lstrip(), Rstrip() TryParse(), TryParseHex() Base64Encoder.GetChars(), Base64Decoder.GetBytes() 通常の実装と比べほとんど書き換える必要はありませんが、効果は限定的です。 (およそ5~10%程度) また、前章のように ITextFileParser.ParseLines() 全体に Burst を適用する場合、このような関数ポインタを経由した実装が混入すると Burst は関数ポイントを跨いだ最適化を行えません。 効果が限定的であること、また誤って使用すると最適化に悪影響を及ぼす可能性があることから、こちらの定義済み delegade は将来的に削除したほうがいいかもしれません。 Burst 適用への道 ▽関数ポインタを使う前に ライブラリ全体のデザインは前回の記事と同様なので、以下ではさらに Burst を適用するにあたってつまずいた点について解説します。 今回の事例でいえば、NativeStringCollections ではテキストバイナリのデコードに System.Text.Encodingを, ジョブの時間計測に System.Diagnostics.Stopwatch を使用しており、これらの参照型を持ち込むせいで Unity 公式が推奨する Job 構造体の Burst 適用ができません。 以下はこのように、何らかの理由で JobSystem を用いた Burst 適用ができない場合に、関数ポインタ経由で Burst を利用する場合の Tips 集となります。 公式ドキュメントにもある通り、Burst は JobSystem に適用する場合に最大のパフォーマンスを得られます。手間のかかる小細工を弄する前に、標準的な手法で実現可能な課題かどうかしっかり検討しましょう。 ▽System.Char は Burst で使えない そもそも単純な struct であるはずの Char がなぜ Burst で使えないのかというと、Burst が実際にコードを生成する C++ では基本的に char を 8bit整数(エンコーディングは実装依存!) として取り扱っており、一方 C# では 16bit整数(UTF16) を採用したため、単なる数値としてではなく 文字コードとして考えると適当に相互コピーしたのでは破綻するためと考えられます。 これはC++のDLLなどで文字列解析ライブラリ等を使おうとするなら重大な問題ですが、今回はすべての実装をC#で書いてしまってそれをBurstでトランスコードする形になるので、内部のビット表現が C# のCharと同じ符号なし16bit整数である UInt16 をメンバに持つ struct Char16 を作り、これをベースに NativeStringList や StringEntity, Split(), TryParse()... などを書き換えました。 Char16.cs(抜粋) public struct Char16 { private UInt16 Value; public Char16(Char16 c) { Value = c; } public Char16(char c) { Value = c; } public static implicit operator Char16(char c) {return new Char16 { Value = (UInt16)c } } public static implicit operator char(Char16 c) {return (char)c.Value; } // そのほか、byte, UInt16 との相互変換、 // Equals(), 比較演算子(==, !=, <=, <, >=, >)などを一通り実装 } これを NativeStringCollections では UTF-16 の符号を格納する struct として扱いますが、Burstコンパイラからは単なる UInt16 (ushort) 型に見えるので、Burstにコンパイルさせることができるようになります。 ▽NativeContainer はそのままでは Burst に持っていけない BurstはC#の参照型を一切扱えず、操作できる型はプリミティブ型と unmanaged struct,およびそれらのポインタに限定されます。 一方でUnityが用意している NativeContainer はスレッドセーフおよびメモリリークの監視を実現するために内部で参照型を使用しており、このためNativeContainerを含む struct をそのまま Burst コンパイルの delegade に渡してしまうと、 MarshalDirectiveException: Type System.Diagnostics.StackTrace which is passed to unmanaged code must have a StructLayout attribute. と実行時にエラーが起きて止まります。普段NativeContainerでやらかす時と異なり、NativeContainerではなくC#のマーシャラー(C# <-> C++ でデータのやり取りの仲介を行うライブラリ)からエラーが飛んでくるのでちょっとわかりづらいです。 事象の深堀はおいておいて、扱いやすいバッファである NativeList を持ち込めれば大変便利なので、これをBurstが扱えるように加工します。 Collections パッケージを Package Manager からインストールすると、Unity 2019.4.24 ではプロジェクトフォルダの `Library/PackageCache/com.unity.collections@0.9.0-preview.6/Unity.Collections にダウンロードされ、ここでNativeContainerの実装を見ることができます。 すると NativeList の正体は以下のような、UnsafeList へのポインタを持つだけのものです。 NativeList.cs(抜粋) public unsafe struct NativeList<T> where T : struct { [NativeDisableUnsafePtrRestriction] internal UnsafeList* m_ListData; public UnsafeList* GetUnsafeList() => m_ListData; }; データの実体に対してはポインタのポインタで関節参照していることになるので、インデクサによるアクセスやLengthフィールドの取得が妙に遅かったのはそういうことか、などという余談はさておき、 この UnsafeListはCapacityと要素数を管理するだけの最小構成に近い実装で、これ単体では Add<T>() はできますがインデクサがありません。ただ、NativeList<T>.GetUnsafeList() というAPIが用意されており、NativeListが管理しているUnsafeListのポインタを取り出すことができます。 これに public T this[int index] などの NativeList に近い挙動のラッパーを被せたものとして UnsafeRefToNativeList<T> を作ります。 UnsafeRefToNativeList.cs(抜粋) namespace NativeStringCollections.Utility { // UnsafeRef を簡単に作成するための拡張メソッド public static class NativeListExt { public static UnsafeRefToNativeList<T> GetUnsafeRef<T>(this NativeList<T> target) where T : unmanaged { return new UnsafeRefToNativeList<T>(target); } } /// <summary> /// This unsafe reference disables the NativeContiner safety system. /// Use only for passing reference to BurstCompiler.CompileFunctionPointer. /// </summary> /// <typeparam name="T"></typeparam> [StructLayout(LayoutKind.Sequential)] public unsafe struct UnsafeRefToNativeList<T> where T : unmanaged { [NativeDisableUnsafePtrRestriction] internal UnsafeList* _list; /// <summary> /// Create the unsafe reference to NativeList<T>. /// </summary> /// <param name="passed_list"></param> public UnsafeRefToNativeList(NativeList<T> passed_list) { // NativeList<T> 内部の UnsafeList へのポインタを取得 // 参照先が共通なので UnsafeRef への変更は元の NativeList にも反映される _list = passed_list.GetUnsafeList(); } public T this[int index] { get { CheckIndexInRange(index, _list->Length); return UnsafeUtility.ReadArrayElement<T>(_list->Ptr, AssumePositive(index)); } set { CheckIndexInRange(index, _list->Length); UnsafeUtility.WriteArrayElement(_list->Ptr, AssumePositive(index), value); } } /* 以下、NativeList.cs の実装で必要そうなものをコピー */ } public unsafe static class UnsafeViewForNativeListUtility { public static void* GetUnsafePtr<T>(this UnsafeRefToNativeList<T> list) where T : unmanaged { return list._list->Ptr; } } } これで NativeList<T> を Burst で使える参照 UnsafeRefToNativeList<T> に変換できるようになりました。 これを使えば、NativeList<T> を内部バッファに使用している NativeJaggedArray<T> や NativeStringList のUnsafeRef も容易に作成できます。 また、自分でUnsafeなコンテナを作るのではなく、領域の確保はNativeListにやらせてBurstに渡す時にだけ型変換する、という方針をとることで、メモリリークの監視やBurstの外でのレースコンディションの検知など、NativeContainerの安全システムの恩恵に与ることができます。 ▽Boxing に注意 Burst は参照型を扱えないため、関数の引数定義によってBoxingが発生したりすると参照型への変換ができないためにコンパイルエラーとなります。メタプログラミングを意識してインターフェイスを利用している場合にはその使い方に注意が必要です。 // interface public interface IPtrGetter { public void* GetUnsafePtr(); } // 普通のInterfaceの使い方 -> Boxing が起きる // Burst でコンパイルできない public unsafe void Func1(IPtrGetter source) { var ptr = source.GetUnsafePtr(); /* ポインタを使う処理 */ } // Generic Interface の書き方 -> struct を渡す時には参照型を生成しない (高速) // Burst でコンパイルしてさらに速くできる public unsafe void Func2<T>(T source) where T : IPtrGetter { var ptr = source.GetUnsafePtr(); /* ポインタを使う処理 */ } ▽Burst がコンパイルする delegade の引数はジェネリックにできない Generic Interface を使えるようになると何となく必要な型制約だけをつけたGenericな引数で関数を定義したくなりますよね。 しかし、Burstのコンパイル結果は delegade 型の変数に格納される = この時点関数ポインタのエントリポイントの型情報が確定している というわけで型情報を確定できない Generic な引数は使えません。 もっとも、この制約はC#とC++の境界になる関数ポインタへの引数だけなので、下記の例のように delegade を呼ぶ関数をオーバーロードしたり、Burstでコンパイルされる関数内で Generic な関数を使うことに問題はありません。 // BurstFunc が受け取れる型 Data public struct Data { }; // Data への暗黙の型変換を実装した型 public struct DataType2 { public implicit operator Data(DataType2 data) {return new Data(/* converion */)}; }; // 関数のオーバーロードで該当するメンバを取り出して渡す例 public struct DataType3 { public Data data_member; }; [BurstCompile] public static class BurstFunc { private void FuncDelegade(ref Data data); private static FuncDelegade _funcDelegade; [RuntimeInitializeOnLoadMethod] public static void Initialize() { _funcDelegade = BurstCompiler. CompileFunctionPointer<FuncDelegade>(FuncBurst).Invoke; } // Burst 関数ポインタへのエントリポイント [BurstCompile] [AOT.MonoPInvokeCallback(typeof(FuncDelegade))] private static void FuncBurst(ref Data data) { var report = Proccess(data); } // Burstへのエントリポイント以外では参照渡ししなくても、 // Genericな関数を使っても、 // void以外を返してもよい private static bool Proccess<T>(T data) { /* 何かする */ return true; } // Data と DataType2 を渡せる public static void Func(ref Data data) { _funcDelegade(ref data); } // DataType3 を渡せる public static void Func(ref DataType3 data) { _funcDelegade(ref data.data_member); } } ▽Burst が行う最適化の特徴 Burst は JobSystem などの複数のCPUコアへ処理を割り振るのではなく、CPUコア内の演算実行ユニットにできるだけ処理を詰め込んで処理を高速にしようとする最適化を行います。 一般的にSIMD化とパイプライン化と呼ばれる最適化になります。 〇 SIMD化 我々が普段扱う変数はintやfloatといったものですが、これらのバイナリサイズは32bitです。 一方、今日のPC用CPUでは加減乗除やビット演算など、同じ演算を同時に複数の変数に適用するための機能がついており、これは現行製品ではAVX2(256bit幅, AMDとIntel両社が対応)やAVX512(512bit幅, 現状Intelのみ)などが利用可能です。 iOSやAndroid端末でも Arm NEON として同様の機能が実装されています。 これを用いることで、例えば8つのfloatを別の8つのfloatと足し算する処理(ちょうどクォータニオン2つ分ですね)でfloatの足し算を8回処理しなければならないところ、AVX2が使える環境では全部まとめて1回の処理で計算が終わります。(32bit x 8 = 256bit) 同じ演算でなければ利用できないとか、大量のデータの読み書きができないとデータが来なくて結局計算ができない(=うまくデータがキャッシュに乗っているかが重要。つまりNativeContainerやECSとセットで使わないとそもそも性能を発揮できない)とか制約はありますが、うまく適用できれば大変な高速化になります。 〇 ソフトウェアパイプライニング SIMD化は一回の命令で処理できるデータを増やす方法でしたが、CPUの命令にはもう一つ特徴があり、たいていの命令は実行してから結果が得られるまでの時間(=Latency)と、次の命令を開始できるようになる時間(=Throughput)が違うことが多いです。 たとえば Latency=5, Throughput=2 の命令を使うとして、いちいち結果が出るのを待ってから次の命令を開始していると20クロックで4回しか処理できませんが、結果に依存性がないデータをThroughputが許す限り、2クロックごとに詰め込めるだけ詰め込んでいけば19クロック時点で8回目のデータの結果が得られます。 こちらも大きな性能向上をもたらす可能性があり、しかしやはりデータが到着しなければ処理が始まりませんので、データがちゃんとキャッシュメモリに(以下略 結局Burstが最適化しやすい処理とは? SIMD化もパイプライニングも、マルチコアでの並列化と同様、同時に処理しているデータの処理結果について互いに依存関係にないことが前提として必要です。 つまり得意な処理対象も同じ、できるだけ大きなforループです。 今回のテーマである文字列のパース処理は、極端に単純化すれば文字列の先頭から1文字ずつ比較していって合致した場所をさがすという超巨大なforループです。 比較的Burstが得意な処理に該当するので、 ITextFileParser.ParseLines() をBurst化した時には2倍近い高速化が達成できました。 一方で、NativeStringCollections.BurstFunc の delegate を使用した場合にさほど大きな性能向上がなかったのは、ParseLines()ではブロックごとの処理内容がそのまま渡されるために数千文字分の巨大なループとして処理を詰め込めたのに対し、Split() や TryParse() 関数単独ではせいぜい数文字から数十文字といったところでCPUコアの演算器を埋め尽くすにはデータが足りなかったと考えられます。 参考資料 Basics of NativeStringCollections: [Unity] C# JobSystem を利用してテキストファイルを非同期でパースする Qiita: 【Unity】BurstCompilerをJobSystem以外でも使いたい 【Unity】NativeArrayについての解説及び実装コードを読んでみる Blog: 相互運用時に戻り値にNon-Bittable型が含まれる場合例外が発生する件 【C#】え、Generic Interfaceでメソッド引数を設定すれば構造体のBoxingを回避できるの? Official: Burst User Guide Scripting API / Unity.Colections
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む