Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

Avatar for Takaaki Suzuki

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# がダックタイピングを採用していることを利用したハック 他にもまだまだ色々…