Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CEDEC 2018 最速のC#の書き方 - C#大統一理論へ向けて性能的課題を払拭する

Yoshifumi Kawai
August 22, 2018
940

CEDEC 2018 最速のC#の書き方 - C#大統一理論へ向けて性能的課題を払拭する

Yoshifumi Kawai

August 22, 2018
Tweet

More Decks by Yoshifumi Kawai

Transcript

  1. Reactive Extensions for Unity https://github.com/neuecc/UniRx/ async/await(UniTask) async UniTask<string> DemoAsync() {

    // You can await Unity's AsyncObject var asset = await Resources.LoadAsync<TextAsset>("foo"); // .ConfigureAwait accepts progress callback await SceneManager.LoadSceneAsync("scene2").ConfigureAwai // await frame-based operation(you can also await frame c await UniTask.Delay(TimeSpan.FromSeconds(3)); // like 'yield return WaitForEndOfFrame', or Rx's Observe await UniTask.Yield(PlayerLoopTiming.PostLateUpdate); // You can await standard task await Task.Run(() => 100); // get async webrequest async UniTask<string> GetTextAsync(UnityWebRequest req) { var op = await req.SendWebRequest(); return op.downloadHandler.text; } var task1 = GetTextAsync(UnityWebRequest.Get("http://goog var task2 = GetTextAsync(UnityWebRequest.Get("http://bing var task3 = GetTextAsync(UnityWebRequest.Get("http://yaho // concurrent async-wait and get result easily by tuple s var (google, bing, yahoo) = await UniTask.WhenAll(task1, // You can handle timeout easily await GetTextAsync(UnityWebRequest.Get("http://unity.com"
  2. Performance by Default というUnityの標語 ECS + Job System + Burst

    Compilerで、C#をC++よりも高速に 性能の向上は不可能を可能にする! C# 7.x + .NET Core 2.1 Linuxで動く.NET実装(登場からかなり時間も経ち十分現実的です) OSS化により細かい性能向上PRを受け入れ、 人海戦術でボトルネックが潰されていっている 言語やランタイムにも手を入れ大きな性能向上を果たしている
  3. // ふつーのシリアライザのAPIの例 byte[] Serialize<T>(T obj) { // 1. 内部での書き込みストリーム作りのためにnew MemoryStream

    using(var stream = new MemoryStream()) // 2. データ生成時の内部ステートを保持するためのWriterのnew var writer = new XxxWriter(stream); // 3. Int用子シリアライザの取得あるいはprimitiveの場合はswitch var serializer = serializerCacheDictionary[typeof(T)]; // 4. (意外と内部では入ってることがある)objectへのボクシング serializer.WriteObject(writer, (object)obj); // 5. 可変長整数へのエンコード if(x <10) write... else if(x < 150) write... // 6. WriteByte呼び出しの連打(内部では幾つかのifやインクリメント) stream.WriteByte(byte >> 0); stream.WriteByte(byte >> 8) ... // 7. MemoryStreamのToArrayはbyte[]コピー memoryStream.ToArray(); }
  4. // ふつーのシリアライザのAPIの例 byte[] Serialize<T>(T obj) { // 1. 内部での書き込みストリーム作りのためにnew MemoryStream

    using(var stream = new MemoryStream()) // 2. データ生成時の内部ステートを保持するためのWriterのnew var writer = new XxxWriter(stream); // 3. Int用子シリアライザの取得あるいはprimitiveの場合はswitch var serializer = serializerCacheDictionary[typeof(T)]; // 4. (意外と内部では入ってることがある)objectへのボクシング serializer.WriteObject(writer, (object)obj); // 5. 可変長整数へのエンコード if(x <10) write... else if(x < 150) write... // 6. WriteByte呼び出しの連打(内部では幾つかのifやインクリメント) stream.WriteByte(byte >> 0); stream.WriteByte(byte >> 8) ... // 7. MemoryStreamのToArrayはbyte[]コピー memoryStream.ToArray(); } そりゃ遅い! けれどそれは言われてみれば…… というのも事実 そして逆に言えば これを全部避ければ速いはず!
  5. // こんなクラスがあるとして public class Sample { public int Id {

    get; set; } public string Name { get; set; } public string[] Addresses { get; set; } } // Object作って Sample obj = new Sample { Id = 10, Name = "Foo", Addresses = new[] { "Foo", "Bar", "Baz" } }; // こんな感じにbyte[]に変換するというAPI byte[] bin = MessagePackSerializer.Serialize<Sample>(obj);
  6. // これを詳細にバラすと byte[] bin = MessagePackSerializer.Serialize<Sample>(obj); // Sampleの子シリアライザを取得し var sampleFormatter

    = StandardResolver.Instance.GetFormatter<Sample>(); // resultの参照 byte[] bin = null; // こんな風になっている(refによってbinに結果が詰まってくる) sampleFormatter.Serialize(ref bin, 0, obj, StandardResolver.Instance);
  7. sampleFormatter.Serialize(ref bin, 0, obj, StandardResolver.Instance); // 子シリアライザ取得のための入れ物(後述) IFormatterResolver resolver =

    StandardResolver.Instance; // メモリプールから作業用byte[]を取得 var bin = BufferPool.ThreadStaticBuffer; // byte[]上の0位置から書き込み開始 var offset = 0; // オブジェクトの線形化 -> 配列上にならべる[Id, Name, Addresses] offset += MessagePackBinary.WriteArrayHeader(ref bin, offset, 3); // intのプリミティブバイナリ化 offset += MessagePackBinary.WriteInt32(ref bin, offset, obj.Id); // stringのプリミティブバイナリ化 offset += MessagePackBinary.WriteString(ref bin, offset, obj.Name); // string[]の子シリアライザを取得 var addressessFormatter = resolver.GetFormatter<string[]>(); offset += addressessFormatter.Serialize(ref bin, offset, obj.Addresses, resolver); // 新規にbyte[]を作り作業用byte[]からコピー var finalBytes = new byte[offset]; Buffer.BlockCopy(bin, 0, finalBytes, 0, offset); return finalBytes;
  8. sampleFormatter.Serialize(ref bin, 0, obj, StandardResolver.Instance); // 子シリアライザ取得のための入れ物(後述) IFormatterResolver resolver =

    StandardResolver.Instance; // メモリプールから作業用byte[]を取得 var bin = BufferPool.ThreadStaticBuffer; // byte[]上の0位置から書き込み開始 var offset = 0; // オブジェクトの線形化 -> 配列上にならべる[Id, Name, Addresses] offset += MessagePackBinary.WriteArrayHeader(ref bin, offset, 3); // intのプリミティブバイナリ化 offset += MessagePackBinary.WriteInt32(ref bin, offset, obj.Id); // stringのプリミティブバイナリ化 offset += MessagePackBinary.WriteString(ref bin, offset, obj.Name); // string[]の子シリアライザを取得 var addressessFormatter = resolver.GetFormatter<string[]>(); offset += addressessFormatter.Serialize(ref bin, offset, obj.Addresses, resolver); // 新規にbyte[]を作り作業用byte[]からコピー var finalBytes = new byte[offset]; Buffer.BlockCopy(bin, 0, finalBytes, 0, offset); return finalBytes;
  9. public class SampleResolver : IFormatterResolver { public static readonly IFormatterResolver

    Instance = new SampleResolver(); SampleResolver() { } public IMessagePackFormatter<T> GetFormatter<T>() { // Dictionaryのlookupのかわりに<T>.fieldから取ってくる // 処理効率はJITコンパイラに委ねられ、C#のレイヤーでどうこうするより圧倒的に速い return Cache<T>.formatter; } static class Cache<T> { public static readonly IMessagePackFormatter<T> formatter; // 静的コンストラクタはスレッドセーフで必ず一度しか呼ばれないことが言語的に保証されている static Cache() { var t = typeof(T); if (t == typeof(int)) formatter = new Int32Formatter(); else if (t == typeof(string)) formatter = new NullableStringFormatter(); else .... } } }
  10. public class SampleResolver : IFormatterResolver { public static readonly IFormatterResolver

    Instance = new SampleResolver(); SampleResolver() { } public IMessagePackFormatter<T> GetFormatter<T>() { // Dictionaryのlookupのかわりに<T>.fieldから取ってくる // 処理効率はJITコンパイラに委ねられ、C#のレイヤーでどうこうするより圧倒的に速い return Cache<T>.formatter; } static class Cache<T> { public static readonly IMessagePackFormatter<T> formatter; // 静的コンストラクタはスレッドセーフで必ず一度しか呼ばれないことが言語的に保証されている static Cache() { var t = typeof(T); if (t == typeof(int)) formatter = new Int32Formatter(); else if (t == typeof(string)) formatter = new NullableStringFormatter(); else .... } } }
  11. ジェネリック型はIL2CPPでコードサイズが膨らむ static class Cache<T> { public static readonly IMessagePackFormatter<T> formatter;

    // この中身のコードはTが値型の場合、同じものが吐かれる(参照型ならば共有される) // 値型はPrimitiveだけじゃなくEnumなども含まれるため、場合によっては凄い量になる…… static Cache() { // もしこの中身が凄い多い場合かなりのことになる // 実際UnityのIL2CPPでDictionary<TKey, TValue>はバイナリサイズが膨らむ要因の一つ! var t = typeof(T); if (t == typeof(int)) formatter = new Int32Formatter(); else if (t == typeof(string)) formatter = new NullableStringFormatter(); else .... } }
  12. static class Cache<T> { public static readonly IMessagePackFormatter<T> formatter; static

    Cache() { var f = CacheHelper.CreateFormatter(typeof(T)); if (f != null) { formatter = (IMessagePackFormatter<T>)f; } } } static class CacheHelper { public static object CreateFormatter(Type t) { if (t == typeof(int)) return new Int32Formatter(); else if (t == typeof(string)) return new NullableStringFormatter(); else .... return null; }
  13. メソッドの直接渡しは暗黙的デリゲート生成 public class Foo { public void Bar() { //

    これは ThreadPool.QueueUserWorkItem(RunInThreadPool); // これに等しい ThreadPool.QueueUserWorkItem(new WaitCallback(RunInThreadPool)); } void RunInThreadPool(object _) { Console.WriteLine("foo"); } }
  14. Action<object>の使い方 public int X; public void Bar() { // 「インスタンス変数」などを使いたい場合staticなキャッシュは使えない

    // しかし勿論これではデリゲートを生成してしまっている ThreadPool.QueueUserWorkItem(_ => Nanikasuru()); } void Nanikasuru() { Console.WriteLine("Nanika Suru:" + X); }
  15. static readonly WaitCallback CallBack = RunInThreadPool; public int X; public

    void Bar() { // Action<object>のstateはそのためにある、thisを入れるのが頻出パターン。 // これはキャッシュされたデリゲートを使うためゼロアロケーション ThreadPool.QueueUserWorkItem(CallBack, this); } static void RunInThreadPool(object state) { // stateからthisを引っ張ることでインスタンスメソッドを呼び出せる var self = (Foo)state; self.Nanikasuru(); } void Nanikasuru() { Console.WriteLine("Nanika Suru:" + X); }
  16. ラムダ式のキャプチャ static void Run(List<int> list, string format) { // ラムダ式のキャプチャ(外側の変数を中で使うこと)は暗黙的なクラス生成が入る

    // 以下のようなコードが生成されるので、クラスとデリゲートの二つがアロケート対象。 // var capture = new { format }; // new Action<int>(capture.Run); list.ForEach(x => { Console.WriteLine(string.Format(format, x)); }); }
  17. ラムダ式のキャプチャ static void Run(List<int> list, string format) { // ラムダ式のキャプチャ(外側の変数を中で使うこと)は暗黙的なクラス生成が入る

    // 以下のようなコードが生成されるので、クラスとデリゲートの二つがアロケート対象。 // var capture = new { format }; // new Action<int>(capture.Run); list.ForEach(x => { Console.WriteLine(string.Format(format, x)); }); }
  18. static void Run(List<int> list, string format, bool cond) { //

    does not use lambda, but... if (cond) { Console.WriteLine("Do Nothing"); return; } // use capture path list.ForEach(x => { Console.WriteLine(string.Format(format, x)); }); }
  19. static void Run(List<int> list, string format, bool cond) { //

    does not use lambda, but... if (cond) { Console.WriteLine("Do Nothing"); return; } // use capture path list.ForEach(x => { Console.WriteLine(string.Format(format, x)); }); }
  20. static void Run(List<int> list, string format, bool cond) { if

    (cond) { Console.WriteLine("Do Nothing"); return; } // キャプチャが存在するメソッドを分けることで回避 RunCore(list, format); } static void RunCore(List<int> list, string format) { list.ForEach(x => { Console.WriteLine(string.Format(format, x)); }); }