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

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

Yoshifumi Kawai
August 22, 2018
590

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

Yoshifumi Kawai

August 22, 2018
Tweet

More Decks by Yoshifumi Kawai

Transcript

  1. View Slide

  2. Tweetは是非 #CEDEC2018
    スライドはセッション後すぐ公開します

    View Slide

  3. 河合 宜文 / Kawai Yoshifumi / @neuecc
    New World, Inc.
    C#
    Unity

    View Slide

  4. C#最速シリアライザ
    https://github.com/neuecc/MessagePack-CSharp/

    View Slide

  5. Reactive Extensions for Unity
    https://github.com/neuecc/UniRx/
    async/await(UniTask)
    async UniTask DemoAsync()
    {
    // You can await Unity's AsyncObject
    var asset = await Resources.LoadAsync("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 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"

    View Slide

  6. View Slide

  7. using C

    View Slide

  8. in 10 years

    View Slide

  9. C#大統一理論 #とは

    View Slide

  10. サーバーも
    クライアントも
    全部C#で統一する

    View Slide

  11. サーバーサイドエンジニアでC#で書ける人間がい
    なくて覚えなおし基盤システムがもう構築されて
    るコードが流用できないそもそもWindowsだしい
    やMicrosoftがいや余計にお金がかかりそうプロプ
    ライエタリじゃないのGoのほうがなうい別にPHP
    で困ってないC++のほうがパフォーマンスいいん
    じゃないのノウハウがないインターネットに情報
    がないオープンじゃない気がするMacで動かない
    んじゃないの事例もあんまないし失敗しそうetc...

    View Slide

  12. サーバーサイドエンジニアでC#で書ける人間がい
    なくて覚えなおし基盤システムがもう構築されて
    るコードが流用できないそもそもWindowsだしい
    やMicrosoftがいや余計にお金がかかりそうプロプ
    ライエタリじゃないのGoのほうがなうい別にPHP
    で困ってないC++のほうがパフォーマンスいいん
    じゃないのノウハウがないインターネットに情報
    がないオープンじゃない気がするMacで動かない
    んじゃないの事例もあんまないし失敗しそうetc...

    View Slide

  13. Realtime
    Unity
    API
    Service

    View Slide

  14. Realtime
    Unity
    API
    Service

    View Slide

  15. Realtime
    Unity
    API
    Service

    View Slide

  16. Realtime
    Unity
    API
    Service

    View Slide

  17. Realtime
    Unity
    API
    Service

    View Slide

  18. Realtime
    Unity
    API
    Service

    View Slide

  19. Realtime
    Unity
    API
    Service

    View Slide

  20. Realtime
    Unity
    API
    Service

    View Slide

  21. Realtime
    Unity
    API
    Service

    View Slide

  22. Performance of C#

    View Slide

  23. 速い?
    遅い?

    View Slide

  24. 速い?
    遅い?

    View Slide

  25. やっぱ遅かった。
    当時(15年前)パフォーマンスを志向してこなかった

    View Slide

  26. View Slide

  27. Motivation

    View Slide

  28. Performance by Default
    というUnityの標語
    ECS + Job System + Burst Compilerで、C#をC++よりも高速に
    性能の向上は不可能を可能にする!
    C# 7.x + .NET Core 2.1
    Linuxで動く.NET実装(登場からかなり時間も経ち十分現実的です)
    OSS化により細かい性能向上PRを受け入れ、
    人海戦術でボトルネックが潰されていっている
    言語やランタイムにも手を入れ大きな性能向上を果たしている

    View Slide

  29. View Slide

  30. リアルな性能は基盤だけでは決まらない
    ウェブフレームワークやデータベースアクセス、シリアライゼー
    ション、etc...
    (そもそも言語自体の評価が、言語だけではなくライブラリやエコ
    システムなども含めて評価すべき)
    現実的にはまだC#はよくなってきたとはいえ、手薄
    そこがパフォーマンスを当初からしっかり意識して鍛え上げられて
    きた言語と、最近やっと目覚めたゆとりC#との違い
    だからこそ、やらなければならない

    View Slide

  31. C#最速JSONシリアライザ
    https://github.com/neuecc/Utf8Json

    View Slide

  32. C#も誕生15年、一見枯れてるように見える、が
    別にそんなことはなかった
    C#で真の意味でのパフォーマンスが意識されだしたのは近年
    言語のポテンシャルを100%活かしたライブラリを開発することに
    よって、C#のパフォーマンスを下支えする
    そして性能のベースラインを掲示したことにより、世界中でシリア
    ライザに限らずC#のパフォーマンスへの意識を変えた
    常に前線で競える言語となるために
    誰かがやってくれるのではなく誰もが当事者
    それがオープンであるということだし、C#もそういう世界にいる

    View Slide

  33. Introduction to the
    High Performance C#

    View Slide

  34. NOTICE
    この先の内容はあくまでエクストリーム
    なパフォーマンスを求めたい場合のため
    であり普通に書く場合これらに気をつけ
    る必要は別に特に全くあまりないことも
    多いことは前提としてください

    View Slide

  35. 徹頭徹尾、遅くなる要因を取り除く
    極限的な領域だとプロファイラは役に立たない
    (ぶっちゃけ見ても特に何もネックになっていない)
    理論的に最速になるコードのイメージを描いて、
    全ての言語知識を駆使して通過するコードパスから無駄を省き、
    それに近づける
    狂気に彩られた執念で全てを潰す
    特別なことはないし一つ一つはとにかく地味
    それを死ぬほど徹底する、それが唯一最大の秘訣で、難しいこと

    View Slide

  36. 例えばint(999)をbyte[]にシリアライズ
    var bytes = BitConverter.GetBytes(999);
    unsafe
    {
    var bytes = new byte[4];
    fixed (byte* ptr = bytes)
    {
    *((int*)ptr) = 999;
    }
    }

    View Slide

  37. // ふつーのシリアライザのAPIの例
    byte[] Serialize(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();
    }

    View Slide

  38. // ふつーのシリアライザのAPIの例
    byte[] Serialize(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();
    }
    そりゃ遅い!
    けれどそれは言われてみれば……
    というのも事実
    そして逆に言えば
    これを全部避ければ速いはず!

    View Slide

  39. // こんなクラスがあるとして
    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(obj);

    View Slide

  40. // これを詳細にバラすと
    byte[] bin = MessagePackSerializer.Serialize(obj);
    // Sampleの子シリアライザを取得し
    var sampleFormatter = StandardResolver.Instance.GetFormatter();
    // resultの参照
    byte[] bin = null;
    // こんな風になっている(refによってbinに結果が詰まってくる)
    sampleFormatter.Serialize(ref bin, 0, obj, StandardResolver.Instance);

    View Slide

  41. 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();
    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;

    View Slide

  42. 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();
    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;

    View Slide

  43. Chapter1: Dictionary Hack

    View Slide

  44. 取得にDictionaryはコストゼロのように使いがち
    MessagePack for C#では以下のように型毎のシリアライザを取得
    普通の作り方(?)だとこれを辞書から取ってくる
    Dictionary(ハッシュテーブル)はO(1)
    つまりタダじゃん、やったー!ではない
    O(1)でも内部的な処理量はそこそこあり、それなりに遅い
    (もっといえば汎用的に使えるようになっているため、その汎用化
    部分がコードのパフォーマンスを若干落としてる、詳細は後述)
    IMessagePackFormatter f = resolver.GetFormatter();
    IMessagePackFormatter f = dict[typeof(string[])];

    View Slide

  45. 取得にDictionaryはコストゼロのように使いがち
    MessagePack for C#では以下のように型毎のシリアライザを取得
    普通の作り方(?)だとこれを辞書から取ってくる
    Dictionary(ハッシュテーブル)はO(1)
    つまりタダじゃん、やったー!ではない
    O(1)でも内部的な処理量はそこそこあり、それなりに遅い
    (もっといえば汎用的に使えるようになっているため、その汎用化
    部分がコードのパフォーマンスを若干落としてる、詳細は後述)
    IMessagePackFormatter f = resolver.GetFormatter();
    IMessagePackFormatter f = dict[typeof(string[])];

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  49. static class Cache
    {
    public static readonly IMessagePackFormatter formatter;
    static Cache()
    {
    var f = CacheHelper.CreateFormatter(typeof(T));
    if (f != null)
    {
    formatter = (IMessagePackFormatter)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;
    }

    View Slide

  50. 標準のDictionaryは決して最速ではない
    GetHashCode, Equalsの呼び出しがIEqualityComparer経由とい
    うオーバーヘッドがかなり大きい
    直接呼び出しと、仮想メソッド呼び出しの性能差は確実にある
    辞書が必要なら型を決め打って直接呼び出しにした
    自家製のほうが性能面では明らかに良好
    キーが非ジェネリックな辞書(Dictionary)や
    文字列(Dictionary
    バイト配列(Dictionary)相当のものはよく作り使う

    View Slide

  51. 標準のDictionaryは決して最速ではない
    GetHashCode, Equalsの呼び出しがIEqualityComparer経由とい
    うオーバーヘッドがかなり大きい
    直接呼び出しと、仮想メソッド呼び出しの性能差は確実にある
    辞書が必要なら型を決め打って直接呼び出しにした
    自家製のほうが性能面では明らかに良好
    キーが非ジェネリックな辞書(Dictionary)や
    文字列(Dictionary
    バイト配列(Dictionary)相当のものはよく作り使う

    View Slide

  52. 標準のDictionaryは決して最速ではない
    GetHashCode, Equalsの呼び出しがIEqualityComparer経由と
    いうオーバーヘッドがかなり大きい
    直接呼び出しと、仮想メソッド呼び出しの性能差は確実にある
    辞書が必要なら型を決め打って直接呼び出しにした
    自家製のほうが性能面では明らかに良好
    キーが非ジェネリックな辞書(Dictionary)や
    文字列(Dictionary
    バイト配列(Dictionary)相当のものはよく作り使う

    View Slide

  53. byte[](ArraySegment) のKeyの辞書を作る
    KeyからValueを引くためだけにStringにデコードするのは高コスト
    もし入力がbyte[]なら、デコードせずそのまま参照すればいい

    View Slide

  54. byte[](ArraySegment) のKeyの辞書を作る
    KeyからValueを引くためだけにStringにデコードするのは高コスト
    もし入力がbyte[]なら、デコードせずそのまま参照すればいい
    ハッシュ値算出のアルゴリズムを工夫する
    byte[]からハッシュ値を作るアルゴリズムは色々ある
    MessagePack for C#ではFarmHashを採用
    小さめのサイズの文字列に適しているため用途にマッチしていた
    一般的にはxxHashが万能に最強説がある
    LZ4やZStandard作者によるもの

    View Slide

  55. プロパティ名のマッチングを最適化したい
    MessagePackのMapモード(JSONの{}とほぼ同じ)やUtf8Jsonで
    デシリアライズ時に、キーをどの値としてデコードすべきか
    名前のマッチングが必要
    一々デコードしてのStringがキーの辞書は論外
    byte[]がキーの辞書も良いけれど、この場合更に上の技法がある

    View Slide

  56. View Slide

  57. View Slide

  58. 文字列は重い
    Dictionaryの仕組みとして取得時にGetHashCode -> Equalsが走る
    C#の文字列の中身はUTF16で、つまりbyte[]的なもの
    GetHashCodeで全探索して算出、Equalsで全数比較
    文字列が長ければ長いほどインパクトがあることは頭に留めたい
    例えばint IDに変換して代替できるなら、そのほうがずっと良い
    Enumは重い
    Enumは実態は数値型で生で扱う場合は超軽量ですが
    それ以外は重めの処理が入りがちなので注意
    特にUnityではDictionaryのKeyに使うとボクシングが発生……

    View Slide

  59. 文字列は重い
    Dictionaryの仕組みとして取得時にGetHashCode -> Equalsが走る
    C#の文字列の中身はUTF16で、つまりbyte[]的なもの
    GetHashCodeで全探索して算出、Equalsで全数比較
    文字列が長ければ長いほどインパクトがあることは頭に留めたい
    例えばint IDに変換して代替できるなら、そのほうがずっと良い
    Enumは重い
    Enumは実態は数値型で生で扱う場合は超軽量ですが
    それ以外は重めの処理が入りがちなので注意
    特にUnityではDictionaryのKeyに使うと都度ボクシングが発生……

    View Slide

  60. Chapter2: JIT Hack

    View Slide

  61. C# IL
    C++
    JIT
    ASM

    View Slide

  62. ILは大事だが全てではない
    最初に意識すべきことは、C#はコンパイルされると、
    どのようなILに変化するか
    しかし速度的な真の終着点は、どのようなASMで実行されるか
    CLRの場合はJITコンパイラ、Unityの場合はIL2CPPの結果が大事
    ほとんどの場合は素直な結果になりますが、
    時々最適化が走ってILとは異なる結果になる場合もある
    (EqualityComparer.Defaultのdevirtualizationなどもそう)

    View Slide

  63. View Slide

  64. View Slide

  65. View Slide

  66. インライン化はかなり効くのでうまく使いたい

    View Slide

  67. View Slide

  68. View Slide

  69. View Slide

  70. View Slide

  71. View Slide

  72. Chapter3: IL Hack

    View Slide

  73. C#は油断するとすぐに「何か」を生成する
    C#の便利機能のほとんどが暗黙的な(クラス)生成で成り立っている
    C#erはGeneration 0のGCは無料だとマントラを唱えていた
    しかしそれは幻想であり、現実無料ではない
    更に言えば現状のUnityは世代別GCではないのでより高コスト
    高速化の基本原則はアロケーションを最小限にすること
    よし、便利機能は避けよう
    しかし便利機能と思っていなくても分かりにくい罠も存在する

    View Slide

  74. メソッドの直接渡しは暗黙的デリゲート生成
    public class Foo
    {
    public void Bar()
    {
    // これは
    ThreadPool.QueueUserWorkItem(RunInThreadPool);
    // これに等しい
    ThreadPool.QueueUserWorkItem(new WaitCallback(RunInThreadPool));
    }
    void RunInThreadPool(object _)
    {
    Console.WriteLine("foo");
    }
    }

    View Slide

  75. 可能ならフィールドにキャッシュで回避する
    public class Foo
    {
    // もしstaticだったり、インスタンスメソッドでも何度も使うようなものなら
    // フィールドにキャッシュしてあげるのが良い(ラムダ式の非クロージャの場合はそれを自動で行ってくれる)
    static readonly WaitCallback CallBack = RunInThreadPool;
    public void Bar()
    {
    ThreadPool.QueueUserWorkItem(CallBack);
    }
    static void RunInThreadPool(object _)
    {
    Console.WriteLine("foo");
    }
    }

    View Slide

  76. Actionの使い方
    public int X;
    public void Bar()
    {
    // 「インスタンス変数」などを使いたい場合staticなキャッシュは使えない
    // しかし勿論これではデリゲートを生成してしまっている
    ThreadPool.QueueUserWorkItem(_ => Nanikasuru());
    }
    void Nanikasuru()
    {
    Console.WriteLine("Nanika Suru:" + X);
    }

    View Slide

  77. static readonly WaitCallback CallBack = RunInThreadPool;
    public int X;
    public void Bar()
    {
    // Actionの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);
    }

    View Slide

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

    View Slide

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

    View Slide

  80. static void Run(List 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));
    });
    }

    View Slide

  81. static void Run(List 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));
    });
    }

    View Slide

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

    View Slide

  83. 文字列の連結はString.Concatに化ける
    ループなど複数回連結が発生するならStringBuilder
    全て+で繋ぎきれるなら+で繋げてしまうのが良い
    static void Foo(string a, string b, string c, string d)
    {
    // 以下の形に変換される
    // var z = string.Concat(a, b, c, d);
    var z = a + b + c + d;
    }

    View Slide

  84. View Slide

  85. View Slide

  86. static void Foo(string a, int b, string c)
    {
    var z = a + b + c;
    }

    View Slide

  87. static void Foo(string a, int b, string c)
    {
    var z = a + b + c;
    }

    View Slide

  88. static void Foo(string a, int b, string c)
    {
    var z = a + b.ToString() + c;
    }

    View Slide

  89. Chapter4: Binary Hack

    View Slide

  90. Chapter5: async/await Hack

    View Slide

  91. Chapter6:
    MetaProgramming Hack

    View Slide

  92. Conclusion

    View Slide

  93. C#の言語的なポテンシャルは高い
    フレームワークやライブラリも揃いつつあり、
    確かなパフォーマンスを示せている
    CPU拡張命令を利用した高速化への対応も
    UnityはBurst Compiler
    .NET CoreではSystem.Runtime.Intrinsicsなどで対応しつつある
    C#の言語的なポテンシャルは高い
    Linuxでの安定動作も含めて、いよいよ道具が揃いつつあり
    真のC#元年の幕開けといっても過言ではない

    View Slide

  94. OSSライブラリの開発と公開
    C#を最前線で戦える言語にしていくため武器を磨く
    最高のパフォーマンスで基盤を固め、余力を作り出す
    現実世界で実証する
    技術とプロダクトは両輪、何事も実証とともにあるべき
    C#のへの投資が優れた結果を生み出せることを証明していく
    緩やかな連合の形成
    C#には情報がない、ではなく軸にして公開して協力していく
    向かうべき視線は世界!と、堂々と行きましょう

    View Slide