Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.

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

23 463 vues

Publié le

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

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

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

Publié dans : Technologie
  • Login to see the comments

今日からできる!簡単 .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

×