Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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

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

C# / .NET における、パフォーマンス改善の Tips をお届けします。
これを見れば、効率良く 80 点を取ることができるようになるはずです!

Takaaki Suzuki

April 27, 2024
Tweet

More Decks by Takaaki Suzuki

Other Decks in Technology

Transcript

  1. Name 鈴木 孝明 a.k.a @xin9le Work Application Engineer Award Microsoft

    MVP (2012/7 -) Web Site https://about.xin9le.net About
  2. 顧客への価値提供が最優先 「まず MVP (Minimum Viable Product) から始めよ」は真理 Done is better

    than perfect. そのために見限るものは多い 仕様変更 / 機能削減はソフトウェア開発では茶飯事 その中でパフォーマンスは最も後回しにされがちなものの代表 よくある光景
  3. .First() 好き過ぎ問題 運が悪いと 100 万要素の末尾にあるかもしれない 「インデックスの張り忘れ」と並ぶタイムアウト理由 (当社比 ループの中での線形探索 foreach (var

    student in students) { // O(n) になるこのパターンが本当に多い…! // ラムダ式で変数キャプチャもされるし、良いことがない var score = scores.First(x => x.StudentId == student.Id); // 事前に Dictionary を作っておくだけで O(1) 検索になって爆速 var score = scoreByStudentId[student.Id]; }
  4. 不必要に具象型を要求しない 高速化都合など、メソッド内に不可避な理由がある場合を除く ライブラリ層でもない限り、IE<T> など抽象的な型を使うのが吉 コレクションの型合わせ // 型合わせのための .ToArray() でメモリコピーを無駄に発生させる List<int>

    values = [0, 1, 2, 3, 4]; DoSomething(values.ToArray()); // 型遊びしてないで引数を直せ! // 特段の理由がないのに具象型を要求する引数を作ってしまう static void DoSomething(int[] values) { foreach (var x in values) { } }
  5. 動的なバッファ拡張を極力避ける .Add() しただけで「2 倍のメモリ確保 + 全要素コピー」が発生 もはや下手なコードより LINQ の方がずっと効率的 コレクションの初期容量を指定

    int[] values = [0, 1, 2, 3, 4]; var list = new List<int>(capacity: values.Length); foreach (var x in values) { // 初期容量を指定しない場合、最後の要素を追加するときに // 2 倍の内部バッファ確保と全要素コピーが発生 list.Add(x * x); } 超大事!
  6. .Result / .Wait() はダメ。ゼッタイ。 本質的に 1 スレッドで実行できる処理に 2 スレッド使うことになる 開発フレームワークによってはデッドロックする可能性も

    Sync over async public int DoSomething() { var result = CallDependencyAsync().Result; return result + 1; } public async Task<int> DoSomethingAsync() { var result = await CallDependencyAsync(); return result + 1; }
  7. .NET 8 何年にも渡る地道な改善で .NET Framework より何倍も速い 新規プロジェクトでは完全に一択 C# 12 C#

    7.x 以降はパフォーマンスを意識した改善がたくさん 後方互換があるのでサッサと更新して最新の言語機能を使おう 最速の開発/実行環境
  8. MemoryPack MessagePack for C# より速いバイナリシリアライザ C# のメモリ構造をほぼそのままバイナリデータに変換している MessagePack for C#

    .NET 界最速の MessagePack シリアライザ SignalR の標準シリアライザとして採用されるくらい爆速 最速のシリアライザ
  9. 性能劣化に直結するコピーを抑止 構造体のサイズが大きいときに検討したい 参照渡し (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; }
  10. 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(); // 防衛的コピー }
  11. 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(); // コピーなし }
  12. 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 でのみ 適用可能
  13. コピー抑止 構造体の拡張メソッドを作るときには積極的に参照渡しにしたい 防衛的コピー周りの理由で 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 {}
  14. 参照渡しの演算子 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); }
  15. 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));
  16. ゼロ初期化されたメモリ領域に直書き これまで 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" }
  17. スタック領域に配列を安全に確保 利用期間が短く、小さなサイズの配列を扱いたいときに活躍 unmanage 型限定 / 非同期メソッドの中では利用不可 (※ 式中では OK) stackalloc

    // byte 配列に乱数を格納 Span<byte> buffer = stackalloc byte[64]; Random.Shared.NextBytes(buffer); // ファイルに書き込み using var file = File.OpenWrite(path); file.Write(buffer);
  18. Box 化の雨あられ Generics のない C# 1.0 時代の黒歴史 値型を扱うと特に遅くて全く使い物にならない (10 倍以上遅い)

    今すぐ殲滅せよ! 変更先は System.Collections.Generics もう使っている人はいないと信じたい 産廃 : System.Collections
  19. 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 : struct, Enum {}
  20. 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();
  21. ヒープ確保と利便性とのトレードオフ LINQ やイベントコールバックなどで高頻度で利用 よく通るコードパスでは敢えてキャプチャしない書き方も検討 変数キャプチャ = 隠しクラス // 変数 id

    が FirstOrDefault にキャプチャされている static async Task<Person> GetAsync(int id) { var people = await QueryFromDbAsync(); return people.FirstOrDefault(x => x.Id == id); }
  22. 拡張メソッドを自作すると便利 // ラムダ式の中で利用したい変数を state として別途渡すことでキャプチャを回避 public static T? FirstOrDefault<T, TState>(

    this IEnumerable<T> source, TState state, Func<T, TState, bool> predicate) { foreach (var x in source) { if (predicate(x, state)) return x; } return default; } // こんな感じで使う .FirstOrDefault(id, static (x, state) => x.Id == state);
  23. 意図しない変数キャプチャを防止 ローカル関数のキャプチャは高速だけど若干のペナルティはある 静的ローカル関数 (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; // コンパイルエラー }
  24. キャプチャ防止 + デリゲートキャッシュ static がないと不安になるくらい溺愛すべし! 静的ラムダ式 (C# 9.0) var x

    = 10; var result = Enumerable.Range(0, 10) .Where(x => x % 2 is 0) .ToDictionary(x => x, y => x * x); var x = 10; var result = Enumerable.Range(0, 10) .Where(static x => x % 2 is 0) .ToDictionary(static x => x, static y => x * x); static の加護 でエラーに 気付き難い バグ
  25. 変数キャプチャの外出し 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);
  26. async の正体は State Machine コンパイラがひっそりと class / struct を作って実現している await

    する必要がないなら async を積極的に消す 不要な async 修飾子 // 隠れインスタンスが生成される static async Task DoAsync() => await Task.Delay(300); // 推奨 : これは全く無駄がない static Task DoAsync() => Task.Delay(300); // async だけ付いてるのも無駄 // 警告 (CS1998) を無視しない static async Task DoAsync() { // await しなくても動くけど }
  27. 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);
  28. 処理時間を大幅に短縮できる それぞれの処理が独立していることが前提 並列処理を積極的に検討 // 直列 await HogeAsync(); await FugaAsync(); await

    MogeAsync(); // 並列 var t1 = HogeAsync(); var t2 = FugaAsync(); var t3 = MogeAsync(); await Task.WhenAll(t1, t2, t3); t t Parallel 型の 利用も検討
  29. Task / IValueTaskSource をラップした構造体 await を跨がない限り内部に持つ Task 型を生成しない 原則 ValueTask

    で統一するくらいの気持ちで積極的に使う await を通らない可能性を考慮 他人に主導権があるコードはどう実装されるか分からない ex.) abstract / virtual / interface メソッド ValueTask
  30. 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
  31. Static Type Caching public static class FastEnum { public static

    IReadOnlyList<T> GetValues<T>() where T : struct, Enum => Cache<T>.Values; // キャッシュを直接参照 // 静的 Generics 型のフィールドにキャッシュを持つ private static class Cache<T> where T : struct, Enum { public static readonly T[] Values; static Cache() => Values = (T[])Enum.GetValues(typeof(T)); } } T 型ごとに 別の型扱い
  32. 呼び出しコスト ≒ 0 Dictionary<Type, Xxx> から Lookup するよりもずっと速い BenchmarkDotNet が「計測できない」と音を上げるレベル

    静的コンストラクタで事前計算 静的コンストラクタはスレッドセーフが保証されている ロックフリーにできるので、呼び出しコスト低減に大きく寄与 Static Type Caching
  33. .NET Core 時代は配列も再利用 頻繁に発生するメモリの確保と破棄はパフォーマンス悪化のもと .NET Core / .NET Standard (2.1

    以降) では積極的に活用すべし ArrayPool<T> var array = ArrayPool<int>.Shared.Rent(50); try { // レンタルした配列を使って何かする } finally { ArrayPool<int>.Shared.Return(array); }
  34. DefaultInterpolatedStringHandler の活用 Append しかできないが、アロケーションなしで高効率 構造体版の StringBuilder string result; var builder

    = new DefaultInterpolatedStringHandler(0, 0); try { builder.AppendLiteral("現在時刻 : "); builder.AppendFormatted(DateTimeOffset.Now); } finally { result = builder.ToStringAndClear(); } // 現在時刻 : 2024/04/14 23:17:25 +09:00
  35. 内部バッファを Span<T> で直接触る virtual call や Enumerator を介したループを排除 境界値チェックも外れる List<T>

    の高速なイテレーション List<int> list = [0, 1, 2, 3, 4]; var span = CollectionsMarshal.AsSpan(list); foreach (var x in span) { // Do something with x } Span<T> を 取り出し
  36. Frozen Collection 初速を犠牲にして Read 処理を高速化した特化コレクション NuGet で Backport 版が提供されているので .NET

    Fx でも利用可能 NuGet Gallery | System.Collections.Immutable NO MORE yield return; IEnumerator<T> を構造体で自作することでアロケーション削減 C# がダックタイピングを採用していることを利用したハック 他にもまだまだ色々…