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.

今日からできる!簡単 .NET 高速化 Tips

9,446 views

Published on

Event : Visual Studio Users Community Japan #1
Date : 2019/09/14

ソフトウェア/サービス開発において最も後回しにされるものの代表が「パフォーマンスの向上」です。C#/.NET の最大の武器は開発生産性ですが、C# 7.0 以降はパフォーマンス向上のための機能追加が多数行われています。いくつかのポイントを押さえることで実装時からより高速なコードを書くことができるようになります。

このドキュメントでは、そんなポイントとなる箇所をふんだんにお届けします。

Published in: Technology
  • Be the first to comment

今日からできる!簡単 .NET 高速化 Tips

  1. 1. Visual Studio Users Community Japan #1 鈴木 孝明 今日からできる! 簡単 .NET 高速化 Tips
  2. 2. Name 鈴木 孝明 a.k.a @xin9le Work Application Engineer Award Microsoft MVP for Developer Technologies Web Site https://xin9le.net About
  3. 3. Remote Worker @福井 空港がない 陸の孤島
  4. 4. 速さは正義
  5. 5. 顧客への価値提供が最優先 「まず MVP (Minimum Viable Product) から始めよ」は真理 Done is better than perfect. そのために見限るものは多い 仕様変更 / 機能削減はソフトウェア開発では茶飯事 その中でパフォーマンスは最も後回しにされがちなものの代表 よくある光景
  6. 6. ソフトウェアは 速くて困ることはない
  7. 7. 最善手はボトルネック改善 遅い部分を調査/把握して解決すると一気に改善するもの リリース後に問題になってからやるのが一般的 実装時からできることもある ちょっとした工夫でできることはやって損しない 今日はそんなコーディングテクニック集を持ってきました 高速化 = 地道 + チリツモ
  8. 8. 狂気に満ちた世界はこちら (誉め言葉 https://www.slideshare.net/neuecc/cedec-2018-c-c
  9. 9. とりあえず速いものに巻かれろ Use high performance tools
  10. 10. .NET Core 何年にも渡る地道な改善で .NET Framework より何倍も速い 新規プロジェクトでは完全に一択 C# 7.x (or later) C# 7.x はパフォーマンスを意識した改善がほとんど 後方互換があるのでサッサと更新して最新の言語機能を使おう 最速の開発/実行環境
  11. 11. ASP.NET Core + IIS In Process Hosting Model を利用すると現状最速の模様 (by しばやん雑記 ASP.NET Core 2.2 以降の場合、既定で有効 Linux (Docker) よりも 30% ほど速い 速度を取るか、ポータビリティを取るか 最速のホスティング環境 https://blog.shibayan.jp/entry/20181225/1545727432
  12. 12. MessagePack for C# 世界最速の MessagePack シリアライザ SignalR の標準シリアライザとして採用されるくらい爆速 Utf8Json 世界最速の JSON シリアライザ .NET Core 3.0 に入る System.Text.Json より速い 最速のシリアライザ
  13. 13. ASP.NET Core のシリアライザ変更 using AF = Microsoft.AspNetCore.Mvc.Formatters; using UF = Utf8Json.AspNetCoreMvcFormatter; public void ConfigureServices(IServiceCollection services) { services.AddMvc(o => { // Json.NET ベースの ASP.NET Core 標準シリアライザを削除 o.InputFormatters .RemoveType<AF.JsonInputFormatter>(); o.OutputFormatters.RemoveType<AF.JsonOutputFormatter>(); // Utf8Json によるシリアライザを追加 o.InputFormatters .Add(new UF.JsonInputFormatter()); o.OutputFormatters.Add(new UF.JsonOutputFormatter()); }); }
  14. 14. 最速の Enum (FastEnum) https://github.com/xin9le/FastEnum
  15. 15. 構造体を上手に使うのが最近の C# のトレンド Prefer struct
  16. 16. ガベージコレクションの発動頻度を低減 GC の発動はパフォーマンスを瞬間的に大きく低下させる Unity (FPS / VR など) は特に気を配ると良い メンバーアクセスが高速 値型 : スタック領域にあるのでそのまま直アクセス 参照型 : ヒープ領域まで辿ってアクセス 構造体の優位性
  17. 17. 高頻度で値のコピーが発生 引数 / 戻り値 / 変数に入れる / 関数呼び出し サイズの大きい構造体の場合、逆にコストになる 継承 / 多態ができない 現状 interface + 拡張メソッドを駆使するしかない C# 8.0 で interface デフォルト実装が入ると少し状況が変わるかも 構造体の欠点 16 bytes 程度を目途に Defensive Copy
  18. 18. 性能劣化に直結するコピーを抑止 構造体のサイズが大きいときに検討したい 参照渡し (ref / in / out) 常に 気を配る static void Main() { var a = 1; ref var d = ref PassThrough(ref a); d = 2; } static ref int PassThrough(ref int b) { ref var c = ref b; return ref c; }
  19. 19. Defensive Copy の発生 Readonly 保証のためにコピーを作って関数 call をする場合がある コピー回数削減どころか、逆にコピー回数が増えることも in (ref readonly) 引数の難しさ // Bar がフィールドを // 書き換えていない保証がない struct Foo { public readonly int X; public void Bar(){} } // せっかく参照渡ししたけど… void Call(in Foo x) { var a = x.X; // コピーなし x.Bar(); // 防衛的コピー x.Bar(); // 防衛的コピー }
  20. 20. Defensive Copy の抑止 全フィールドが書き換えられないことを保証 readonly struct (C# 7.2) 可能な限り つける // 書き換えていない保証をする readonly struct Foo { public readonly int X; public void Bar(){} } // 読み取り専用の参照渡しが効果を発揮 void Call(in Foo x) { var a = x.X; // コピーなし x.Bar(); // コピーなし x.Bar(); // コピーなし }
  21. 21. Defensive Copy の抑止 関数内でフィールドの書き換えがないことを保証 struct Foo { public int X; public int Y; public readonly int Add() => X + Y; public int Sub() => X - Y; } readonly 関数メンバー (C# 8.0) // 関数単位で挙動が決まる void Call(in Foo x) { x.Add(); // コピーなし x.Sub(); // 防衛的コピー } 可能な限り つける struct でのみ 適用可能
  22. 22. コピー抑止 構造体の拡張メソッドを作るときには積極的に参照渡しにしたい 防衛的コピー周りの理由で Generics における in 引数にはできない 参照渡しの拡張メソッド // OK public static void Foo(ref this int value){} public static void Foo2(in this int value){} // struct 制約があれば ref 引数は OK public static void Foo3<T>(ref this T value) where T : struct {}
  23. 23. 参照渡しの演算子 overload コピー抑止 in 引数のみ認めらているので防衛的コピーに注意 readonly struct Complex { public double R { get; } public double I { get; } public Complex(double r, double i) => (this.R, this.I) = (r, i); // in 引数が認められるようになった public static Complex operator +(in Complex x, in Complex y) => new Complex(x.R + y.R, x.I + y.I); }
  24. 24. Good-bye 匿名型 / Tuple LINQ みたいな局所的な利用 / 多値戻り値には最適 言語機能にも統合されているので書き心地も最高 ValueTuple // ValueTuple : ヒープ未使用 var q1 = collection.Select(x => (value: x, power: x * x)); // 匿名型 : ヒープ利用 var q2 = collection.Select(x => new { Value = x, Power = x * x }); // Tuple : ヒープ利用 (名前を付けられない) var q3 = collection.Select(x => Tuple.Create(x, x * x));
  25. 25. 連続したメモリ領域を直参照 所謂ポインタアクセスを managed に行う仕組み 配列や文字列をメモリ再確保なしに一部分だけ参照できる Span<T> var text = “ab123cd”; var span = text.AsSpan(2, 3); var sub = text.Substring(2, 3); a b 1 2 3 c d 1 2 3 text sub span
  26. 26. ゼロ初期化されたメモリ領域に直書き これまで unsafe を使わないとできなかった最適化 string 初期化時のメモリコピーを削減できるので高速 String.Create static string ToBitString(byte value) => string.Create(8, value, (buffer, state) => { const byte on = 0b_0000_0001; for (var i = 0; i < buffer.Length; i++) { buffer[buffer.Length - 1 - i] = ((state >> i & on) == on) ? '1' : '0'; } }); static void Main() { byte b = 0b_0110_1011; var s = ToBitString(b); // s : "01101011" }
  27. 27. スタック領域に配列を安全に確保 利用期間が短く、小さなサイズの配列を扱いたいときに活躍 unmanage 型限定 / 非同期メソッドの中では利用不可 (※ 式中では OK) stackalloc // byte 配列に乱数を格納 Span<byte> buffer = stackalloc byte[64]; var random = new Random(); random.NextBytes(buffer); // ファイルに書き込み using var file = File.OpenWrite(path); file.Write(buffer);
  28. 28. 「え、それ box 化するんだ?」は勿体ない! Avoid boxing
  29. 29. Box 化の雨あられ Generics のない C# 1.0 時代の黒歴史 値型を扱うと特に遅くて全く使い物にならない (10 倍以上遅い) 今すぐ殲滅せよ! 変更先は System.Collections.Generics もう使っている人はいないと信じたい 産廃 : System.Collections
  30. 30. enum : 値型 / System.Enum : 参照型 System.Enum に値を代入すると box 化する C# 7.3 以降では Generics の enum 制約を使うことで回避できる System.Enum 型の罠 // 引数に渡したときに box 化 static void Foo(Enum value) {} // Generics 制約を使うと box 化しない static void Foo<T>(T value) where T : Enum {}
  31. 31. interface に型変換すると Box 化 ちょっと気を抜くとすぐにヒープ送りにされる Generics を使う or 脱仮想化で box 化を回避 構造体を interface 型として使う // 引数で box 化が発生して遅い static void Interface(IDisposable x) => x.Dispose(); // .NET Core 2.1 以降の場合 // 脱仮想化という最適化がかかる static void NonGeneric(X x) => ((IDisposable)x).Dispose(); // 安定して高速 static void Generic<T>(T x) where T : IDisposable => x.Dispose();
  32. 32. 時間とリソースを有効に使おう Prefer asynchronous
  33. 33. I/O 待ち = CPU が暇してる CPU は最も高価な計算リソース 余裕を持たせ過ぎるではなく十分に「使い切る」ことが重要 非同期 I/O を利用 // RSS を取得している間 CPU が暇 var url = "http://blog.xin9le.net/rss"; var node = XElement.Load(url); // これなら通信待ちを別処理に有効活用できる var url = "http://blog.xin9le.net/rss"; var client = new HttpClient(); var rss = await client.GetStringAsync(url); var node = XElement.Parse(rss);
  34. 34. 処理時間を大幅に短縮できる それぞれの処理が独立していることが前提 並列処理を積極的に検討 // 直列 await HogeAsync(); await FugaAsync(); await MogeAsync(); // 並列 var t1 = HogeAsync(); var t2 = FugaAsync(); var t3 = MogeAsync(); await Task.WhenAll(t1, t2, t3); t t Parallel.For などもある
  35. 35. Task / Task<T> をラップした構造体 await を跨がない限り内部に持つ Task 型を生成しない 原則 ValueTask で統一するくらいの気持ちで積極的に使う await を通らない可能性を考慮 他人に主導権があるコードはどう実装されるか分からない ex.) abstract / virtual / interface メソッド ValueTask
  36. 36. CoreFx は ValueTask の API が乏しい .WhenAll / .WhenAny / .Lazy など、必須級の不足 API を補う .AsTask() するよりもヒープアロケーションを抑えられて高速 ValueTaskSupplement (by Cysharp) // .WhenAll をこんな感じでエレガントに書ける var (foo, bar) = await (FooAsync(), BarAsync()); // 遅延実行もできる (AsyncLazy とか不要) var lazy = ValueTaskEx.Lazy(async () => await Task.Delay(300)); await lazy; https://github.com/Cysharp/ValueTaskSupplement
  37. 37. 知らぬ間にできるインスタンスに気を配る Avoid hidden instancing
  38. 38. ヒープ確保と利便性とのトレードオフ LINQ やイベントコールバックなどで高頻度で利用 よく通るコードパスでは敢えてキャプチャしない書き方も検討 変数キャプチャ = 隠しクラス // 変数 id が FirstOrDefault にキャプチャされている static async Task<Person> GetAsync(int id) { var people = await QueryFromDbAsync(); return people.FirstOrDefault(x => x.Id == id); }
  39. 39. 「=>」にカーソルを合わせる ToolTip にキャプチャされている変数が表示される 変数キャプチャされないコードを書くときの参考に 変数キャプチャを確認
  40. 40. 拡張メソッドを自作すると便利 // ラムダ式の中で利用したい変数を state として別途渡すことでキャプチャを回避 public static T FirstOrDefault<T, TState> (this IEnumerable<T> source, Func<T, TState, bool> predicate, TState state) { if (source == null) throw new ArgumentNullException(nameof(source)); if (predicate == null) throw new ArgumentNullException(nameof(predicate)); foreach (var x in source) if (predicate(x, state)) return x; return default; } // こんな感じで使う .FirstOrDefault((x, state) => x.Id == state, id);
  41. 41. 意図しない変数キャプチャを防止 ローカル関数のキャプチャは高速だけど若干のペナルティはある 静的ローカル関数 (C# 8.0) // これまでのローカル関数 (C# 7.0 以降) static void Main() { var a = 3; var b = 4; var result = LocalFunction(a); int LocalFunction(int x) => x + b; // b をキャプチャしてる } // 静的ローカル関数 (C# 8.0 以降) static void Main() { var a = 3; var b = 4; var result = LocalFunction(a); static int LocalFunction(int x) => x + b; // コンパイルエラー }
  42. 42. 変数キャプチャの外出し static void Main() { // 途中で return していても // 関数の最初で隠しインスタンスが // 生成されてしまう var id = 0; if (true) return; // このコードは通らないのに理不尽! Foo(x => x == id); } static void Main() { // 隠しインスタンスの生成なし var id = 0; if (true) return; CallFoo(id); } // 変数キャプチャされるコードパスを // 別関数にすることで効率化 static void CallFoo(int id) => Foo(x => x == id);
  43. 43. async の正体は State Machine コンパイラがひっそりと class を作って実現している await する必要がないなら async を積極的に消す 不要な async 修飾子 // 隠れインスタンスが生成される static async Task DoAsync() => await Task.Delay(300); // 推奨 : これは全く無駄がない static Task DoAsync() => Task.Delay(300); // async だけ付いてるのも無駄 // 警告 (CS1998) を無視しない static async Task DoAsync() { // await しなくても動くけど }
  44. 44. 他にもある細々したやつ Miscellaneous
  45. 45. .NET Core 時代は配列も再利用 頻繁に発生するメモリの確保と破棄はパフォーマンス悪化のもと .NET Core / .NET Standard (2.1 以降) では積極的に活用すべし ArrayPool<T> var array = ArrayPool<int>.Shared.Rent(50); try { // レンタルした配列を使って何かする } finally { ArrayPool<int>.Shared.Return(array); }
  46. 46. List<T> 好き過ぎ問題 .Add() を連発するとメモリの再確保とコピーが走って遅い 追加が多くインデックスアクセスがないなら LinkedList<T> も検討 そもそも Array でよいのでは、すらある .FirstOrDefault() 好き過ぎ問題 運が悪いと 100 万要素の末尾にあるかもしれない Dictionary にして検索速度を大幅に向上できないか検討 コレクションの特性を知る
  47. 47. GC.Collect(); を意図的に実行しておく 画面遷移 / Loading などの当たり障りない箇所に差し込んでおく インゲーム等での GC 発動を低減し、ユーザー体験を向上 Server GC を有効化 Gen 0 のメモリ領域を多くすることで GC の発生頻度を下げる 高スループットが求められる Web サーバー環境向け ずる賢く GC と付き合う
  48. 48. インライン化 アセンブリサイズは膨らむけど、関数呼び出しを消せる インライン化されるかどうかを判断するのが難し過ぎるので割愛 NO MORE yield return; IEnumerator<T> を構造体で自作することでアロケーション削減 C# がダックタイピングを採用していることを利用したハック 他にもまだまだ色々…
  49. 49. 今日、これだけは持って帰りましょう! Conclusion
  50. 50. 実装時からできることはやって損しない ちょっとポイントを押さえると、実は難しくない! よく通るコードパス / 共通ライブラリに特に目を向ける 速いコードを書くクセをつける ソフトウェアは速くて困ることはない 常に練習と思って意識的に取り組む 高速化 = 地道 + チリツモ
  51. 51. 速さは正義 by アロケーション警察
  52. 52. Enjoy high performance programming using C#!! Thank you

×