Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

C# 7.2 with .NET Core 2.1

389 views

Published on

わんくま同盟 東京勉強会 #110 にて登壇
http://www.wankuma.com/seminar/20180324tokyo110/ <

Published in: Technology
  • Be the first to comment

  • Be the first to like this

C# 7.2 with .NET Core 2.1

  1. 1. C# 7.2 with .NET Core 2.1 岩永 信之
  2. 2. 今日の話 • .NET Core 2.xでのパフォーマンス改善の話 • C# 7.x • .NET Core 2.xのパフォーマンス改善の流れに同調 • 去年リリースされたけど、.NET Core 2.1が出てからが本番
  3. 3. .NET Core 2.1 • https://blogs.msdn.microsoft.com/dotnet/2018/02/27/announci ng-net-core-2-1-preview-1/ • Global Tools • Build Performance Improvements • Minor-Version Roll-forward • Sockets Performance and HTTP Managed Handler • Span<T>, Memory<T> and friends ←今日の話これ • Windows Compatibility Pack
  4. 4. C# 7.2 • http://ufcpp.net/study/csharp/cheatsheet/ap_ver7_2/ • 数字区切り • 非末尾名前付き引数 • private protected • ref readonly/ref拡張メソッド/ref演算子 • ref構造体/Span<T>対応 ←今日の話これ
  5. 5. 前振り: C#/.NETのパフォーマンス
  6. 6. ベンチマーク • 英語で「language benchmark」とかでググると… • こんな感じの サイトが
  7. 7. ベンチマーク(注意点) • 英語で「language benchmark」とかでググると… • https://benchmarksgame.alioth.debian.org/ • 注意点 • 言語をまたいだベンチマークは、言語の差だけじゃなくて 「どのくらい本気の人が参加してるか」による • 実際、C#はちょっと前までいまいちなコードでだいぶ遅かった • レギュレーション難しい • C++は環境依存な関数を容赦なく使ってたりすることが
  8. 8. ベンチマーク(現状) おおむね C, C++ > Rust > Java, Swift, C#, Go > 関数型言語 >>> スクリプト言語 横並び
  9. 9. ベンチマーク(現状からいえること) C#は… .NET Core 2.0でちょっと速くなった • C, C++基準で2~3倍遅い • ここを縮めたいという要望は昔から多い • 特定CPU向け最適化 • GC対策 • スクリプト言語基準で3~5倍速い • にもかかわらず「Node.jsと比べてそんなに速くない」と思われてる • 文字列処理の速度が大差ない • 言語的な差よりも、内部のHTTP処理とかの方がネックになったりする
  10. 10. 要するに、こんな要望になる 特定CPU向け最適化 Web高速化 GC削減 UTF-8 Hardware Intrinsics 参照と構造体活用 Span<T> バイナリ処理 C# 7.0, 7.1 .NET Core 2.1以降 .NET Core 2.1
  11. 11. ということで今日のテーマ • Span<T> (.NET Core 2.1) • Hardware Intrinsics (.NET Core 2.1より後) • おまけ: .NET Core 2.0での細かい最適化
  12. 12. • Span<T> (.NET Core 2.1) • Hardware Intrinsics (.NET Core 2.1より後) • おまけ: .NET Core 2.0での細かい最適化 軽い方から先に
  13. 13. 例えばこんなリファクタリング(1) • uint比較 • 効果 • 比較が1回減る • 1命令と言えど、ループで頻繁に呼ばれる処理なので塵も積もる if ((uint)x < length) 元 後 if (0 < x && x < length)
  14. 14. 例えばこんなリファクタリング(2) • Common Path Optimization (よく通る場所だけ優先最適化) • 効果 • よく通る場所だけはインライン化が効いて速くなる • (長かったり、throw文があるとインライン化されない) if (9割方true) 短い処理 else 長い処理をメソッド抽出(); if (9割方true) 短い処理 else 長い処理 元 後
  15. 15. Q & A (1) • Q. 「この手の細かい改善で5%くらい速くなります」といわれ て、やる? • A. 5%でもやりたい人という人は「世の中にはいる」 • こういうものは、限られた人数では最適化されづらい • コミュニティ貢献が多いらしい • ほんと細かい積み重ねはオープンソース強い
  16. 16. Q & A (2) • Q. いまさら? • A 1. .NET Frameworkからの移行で手一杯だったから… • A 2. ほんと、いまさら • 前述のとおり、こういうのはコミュニティ貢献が結構大きい • 「いまさら」というなら、オープン化のタイミングが… • 方針転換 • 最適化はコンパイラーの仕事ではないか → ホットパスは手作業してでも高速化
  17. 17. • Span<T> (.NET Core 2.1) • Hardware Intrinsics (.NET Core 2.1より後) • おまけ: .NET Core 2.0での細かい最適化 2.1から外れ ちゃったんで… おまけ程度に
  18. 18. C# 対 C言語 • 遅いのはガベージ コレクション(GC)? • → No • スタックの方が速いけどC#でもスタックを主体にしたコードは書ける • そのための機能も年々増えてる(C# 7の構造体活用) • 根本的に(Cでも)ヒープが必要なら、むしろGCは無茶苦茶速い • じゃあ、あと、何が遅いのか? • → 特定CPU向け最適化(CPU専用命令) • 一番犠牲にしてるのはポータビリティ • 要するに、速くしたければ#ifdefだらけ
  19. 19. CPU専用命令の例 • 例: SIMD • single instruction multiple data • Vector命令とか言われたり • 例えばSSE(Pentium IIIの頃に導入された原始的なやつ)ですら • 128ビットレジスターを持ってて • float×4の四則演算を1命令でできる • 単純に考えて個別に4回計算する場合の4倍速い • オーバーヘッドの分を差し引いても倍は速い 単一 命令で 複数 データ を一気に処理
  20. 20. Hardware Instrinsics • 専用命令の使い方 • 普通のコードを書いて、コンパイラーの最適化に任せる? • 案外、その手のコードを生成してくれない • コンパイラーが特殊な関数を用意 • 「この関数呼び出しは、この命令に置き換える」みたいなコンパイルを行う #include <immintrin.h> __m128 c = _mm_mul_ps(a, b) コンパイラーによって 提供されるライブラリ CPU専用命令に置き換わる
  21. 21. Hardware Instrinsics • 専用命令の使い方 • 普通のコードを書いて、コンパイラーの最適化に任せる? • 案外、その手のコードを生成してくれない • コンパイラーが特殊な関数を用意 • 「この関数呼び出しは、この命令に置き換える」みたいなコンパイルを行う #include <immintrin.h> __m128 c = _mm_mul_ps(a, b) こういう関数のことをintrinsicsという • Intrinsic: 内在的な • Hardware Intrinsics: 特定のCPUが内在的に 持っている機能
  22. 22. C#でHardware Intrinsics (1) • パッケージ参照 • .NET Core 2.1 preview 1までは、標準に組み込まれる予定だった • 2.1リリースには間に合わず、現状は • パッケージ参照が必要 • 2.1リリース時点では「プレビュー」なパッケージになる予定
  23. 23. C#でHardware Intrinsics (2) • コードの書き方 using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; result = Sse.Multiply(Sse.Shuffle(a, a, 0x00), Sse.LoadVector128(p result = Sse.Add(result, Sse.Multiply(Sse.Shuffle(a, a, 0x55), Sse result = Sse.Add(result, Sse.Multiply(Sse.Shuffle(a, a, 0xaa), Sse result = Sse.Add(result, Sse.Multiply(Sse.Shuffle(a, a, 0xff), Sse ・・・ 命令セットの 名前のクラス 命令に対応 するメソッド
  24. 24. もちろんCPU依存… • 当然、C言語だと#ifだらけになる • C#でもifだらけになる if (Sse.IsSupported) { return SseAdd(a, b); } else { return Add(a, b); } この命令セットを使える環境の時だけtrue JIT時定数(falseの側は最適化で完全に消える) 使える場合と使えない場合の 2つの実装が必要 かなり速くできるけど、 「ifだらけ」「2重実装」 の覚悟が必要
  25. 25. デモ • 行列の積和でベンチマーク • SIMD命令を使うか使わないかでどのくらい速度差が出るか • https://github.com/ufcpp/UfcppSample/tree/master/Demo/2018/Span Performance/MatrixBenchmark
  26. 26. • Span<T> (.NET Core 2.1) • Hardware Intrinsics (.NET Core 2.1より後) • おまけ: .NET Core 2.0での細かい最適化 本題。 というか今日の話題で2.1 の話題なのはこれだけに
  27. 27. C#対スクリプト言語 • ベンチマーク上は3~5倍は速いはずなのに • Webサーバーとして使うとなんかそこまで差がつかない? • 3~5倍速いのは主に数値計算とか • 文字列処理の速度が大差ない • 言語的な差よりも、内部のHTTP処理(C#やスクリプト言語ではなく、 ネイティブで書かれてる)とかの方がネックになったりする • WebがUTF-8で、C#のstringがUTF-16なのが問題に
  28. 28. Webでの文字列処理の例 • 例えばこんな感じのHTTP GETをしたとして • (アップローダーからJSONをダウンロード) • idとnameの値だけを取り出そうとしたとき… ちなみに、こういうJSON
  29. 29. 文字列処理の現状 • Stream + stringでの処理 … 7B 0D 0A 20 20 22 69 64 22 3A 20 31 2C 0D 0A 20 20 22 6E 61 6D 65 22 3A 20 22 F0 9F 91 A6 F0 9F 8F BD 22 0D 0A 7D … 7B 0D 0A 20 20 22 69 64 22 3A 20 31 2C 0D 0A 20 20 22 6E 61 6D 65 22 3A 20 22 F0 9F 91 A6 F0 9F 8F BD 22 0D 0A 7D Stream.Read(byte[] buffer, int offset, int count) Encoding.GetString(byte[] bytes) … 22 00 69 00 64 00 22 00 3A 00 20 00 31 00 2C 00 0D 00 0A 00 20 00 20 00 22 00 6E 00 61 00 6D 00 65 00 22 00 3A 00 20 00 … string.Substring string.Substring 31 00 1 22 00 3D D8 66 DC 3C D8 FD DF 22 00 "👦🏽" UTF-8 UTF-16
  30. 30. 文字列処理をどうしたいか(1) • Nativeヒープを直接参照 … 7B 0D 0A 20 20 22 69 64 22 3A 20 31 2C 0D 0A 20 20 22 6E 61 6D 65 22 3A 20 22 F0 9F 91 A6 F0 9F 8F BD 22 0D 0A 7D … IPipeReader.TryRead(out ReadResult result) OwnedBuffer 参照管理のための小さいクラス
  31. 31. 文字列処理をどうしたいか(2) • UTF-8のままで処理 … 7B 0D 0A 20 20 22 69 64 22 3A 20 31 2C 0D 0A 20 20 22 6E 61 6D 65 22 3A 20 22 F0 9F 91 A6 F0 9F 8F BD 22 0D 0A 7D … OwnedBuffer Utf8String new Utf8String(buffer.Span)
  32. 32. 文字列処理をどうしたいか(3) • 部分参照でコピーを作らない … 7B 0D 0A 20 20 22 69 64 22 3A 20 31 2C 0D 0A 20 20 22 6E 61 6D 65 22 3A 20 22 F0 9F 91 A6 F0 9F 8F BD 22 0D 0A 7D … OwnedBuffer Utf8String new Utf8String(buffer.Span) Substring Utf8String Utf8String 1 "👦🏽" スタックしか使わない Substringもコピーを作らない 直接intにParse すればヒープ不要 ToString時に 初めてヒープ確保
  33. 33. 何が必要か • メモリの一部分を参照 • Nativeヒープを直接 • stringの一部分 • 参照を前提にしたI/O • UTF-8を直接読み書き Span<T>型 Pipelines Utf8String .NET Core 2.1 その後 依存関係 C# 7.0~7.2 • ref戻り値 • ref安全ルール Span<T>型 Pipelines Utf8String 今ここ
  34. 34. Span<T>: 「範囲」を参照 • 論理的には、参照+長さ … 7B 0D 0A 20 20 22 69 64 22 3A 20 31 2C 0D 0A 20 20 22 6E 61 6D 65 22 3A 20 22 F0 9F 91 A6 F0 9F 8F BD 22 0D 0A 7D … 1 struct Span<T> { ref T Pointer; int Length; } どこから 何要素 例 ここから1バイト ここから10バイト " "
  35. 35. Span<T>: いろんなところ参照 • Managedヒープも、Nativeヒープも、スタックも参照できる Span<byte> span = new byte[N]; Span<byte> span = stackalloc byte[N]; var p = Marshal.AllocHGlobal(N); Span<byte> span = new Span<byte>((byte*)p, N); 普通の配列 = Managedヒープ stackalloc = スタック P/Invokeやunsafe = Nativeヒープ 同じ型で参照できる = 同じロジックで扱える
  36. 36. C# 7.2: Span安全ルール • スタック(ローカル変数)の参照を外に返してはいけない ref int Ok(ref int x) { return ref x; } ref int Ng() { int x; return ref x; } Span<int> Ok(Span<int> x) { return x; } Span<int> Ng() { Span<int> x = stackalloc int[1]; return x; } ref安全ルール(C# 7.0) Span<T>※安全ルール(C# 7.2) ローカル変数 (メソッドを抜けると消える) 消えるものの参照 (返そうとするとエラー) ※ 正確にはSpan<T>専用ではなくて、ref構造体に対して掛かる制限
  37. 37. C# 7.2: 安全なstackalloc • 例: 頻出数字の検索 static int MostFrequentDigit(string s) { var digits = new int[10]; foreach (var c in s) { var d = c - '0'; if ((uint)d < 10) ++digits[d]; } var max = 0; for (int i = 1; i < 10; i++) if (digits[max] < digits[i]) max = i; return max; } 0~9の文字の頻度をカウント するための一時バッファー 配列のせいでヒープ確保 (あんまり好ましくない)
  38. 38. C# 7.2: 安全なstackalloc • 例: 頻出数字の検索 static int MostFrequentDigit(string s) { Span<int> digits = stackalloc int[10]; foreach (var c in s) { var d = c - '0'; if ((uint)d < 10) ++digits[d]; } var max = 0; for (int i = 1; i < 10; i++) if (digits[max] < digits[i]) max = i; return max; } 一時バッファーをnewから stackallocに変更 ヒープ確保がなくなる (速い) unsafe不要 実際、安全 • 範囲チェックあり • 外に返せないルールあり
  39. 39. デモ • Substring • ヒープ確保をなくせば無条件に速くなるというものでもない • GCはほんとに速い • Span<T>があるとだいぶ速い • https://github.com/ufcpp/UfcppSample/tree/master/Demo/2018/Span Performance/SubstringBenchmark
  40. 40. デモ • Split → Join • string.Splitやstring.Joinは一時的なオブジェクトを確保しまくる • Span<T>でヒープ確保なくSplit, Join • (ただし、使い勝手はあまり良くない) • https://github.com/ufcpp/UfcppSample/tree/master/Demo/2018/Span Performance/StringManipulation
  41. 41. まとめ • 2.0~2.1、細かい最適化だらけ • 2.0で実際あったのをちょっと紹介 • Hardware Intrinsics • 2.1から外れちゃったけど • 2.1時点で、プレビュー公開 • Span/UTF-8 • GC速いけど、「ヒープなし」にはさすがに敵わない • そのためのref、Span<T> • C# 7.2で関連機能すでに入ってる/.NET Core 2.1からが本番

×
Save this presentationTap To Close