Open16

C# 最適化/パフォーマンス/Peanut Butter

ピン留めされたアイテム
いぬいぬいぬいぬ

やり方の基本

  1. できるならビルド時最適化を試してみる
  2. アプリ全体でプロファイルする
  3. プロファイルでネックが見つかったら細かく比較計測
  4. Peanut Butterよりアルゴリズム見直し・並列処理化・非同期化・遅延実行

注意点

  • ネック箇所の比較計測には BenchmarkDotNet を使う
    • 事実上標準
    • System.Diagnostics.Stopwatchは辞める
      • 理由
      • めちゃくちゃ時間かかる処理とかはあり
        • StopWatch.GetElapsedTime() (.NET7+
  • ランタイム/コンパイラの違いを意識する
    • 新しい.NETほど同じソースでも高速
      • たとえば、一部のforeachforに事前展開されたりしている
    • SharpLab で 展開後のコードやILの違いを確認できる
    • Unityのランタイムは標準と違うので注意
      • ちょっと古い手法が有効
  • .NET 8.0以降は計測時に Dynamic PGO のON/OFFに注意
    • 良くないコードも Dynamic PGOが効くと最適化されて速くなるから
  • 新しい情報収集を忘れずに

どのやり方をつかう?

重い処理が…

どんな時も

  • アルゴリズムの見直し
    • 1000倍早くなることもよくある

ネットやファイル読み込みが重い

  • 非同期処理 (async / await
  • アルゴリズムの見直し
    • 一度に全部呼び出さない・読み込まない

CPUが重い

  • 一瞬だけ重い
    • 遅延実行( LINQ / Lazy<T>)
  • いつでも重い
    • 並列処理
      • TPL : ParallelFor とか
      • PLINQ : AsParallel()で使える並列LINQ
      • Concurrent Collection : 並列処理用コレクション
      • Synchronization Primitives(同期プリミティブ):semaphoreSlimとかSpinLockとか
      • パーティショナー:TPL/PLINQ用にデータを分割
    • SIMD命令
    • GPUに任せる
      • ComputeSharp
      • Silk.NET (OpenCV)
      • ML.NET
      • OpenTK
いぬいぬいぬいぬ

調べ方:用語

  • 最適化
  • 高速化
  • optimize / optimization
  • performance
  • peanut butter
いぬいぬいぬいぬ

ビルド時最適化

  • そもそもコード中で頑張らなくてもビルド設定でも最適化できる
    • まず最初に見直す
  • 中間コードJITコンパイルのメリットを捨てて最適化する方法、だったり
    • メリットとデメリットの比較が大事!
  • .NETなのでC#以外でも有効

【前提】最新の.NETむけに最新の.NETでビルドする

  • 新しい.NET SDKでコンパイルするとより最適化されたコードが出力される
  • 新しい.NET ランタイムで実行するとより高速な処理になる(SIMD/Dynamic PGOとか)

https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/

R2R (Ready To Run)

https://learn.microsoft.com/ja-jp/dotnet/core/deploying/ready-to-run

  • 事前コンパイルで起動が早くなる
    • この意味ではC#は正確にはJITではなかった
  • 結構前から使える
    • 最新の技術じゃないので使えることが多い
  • ファイルサイズがデカくなる
    • …けど今なら別にいいんじゃないかなぁ
  • 事前コンパイルなのでプラットホーム依存
<PropertyGroup>
  <PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>

NativeAOT

https://learn.microsoft.com/ja-jp/dotnet/core/deploying/native-aot/

  • ネイティブバイナリへの事前コンパイル
    • PublishTrimがデフォでされるらしく容量も小さめになる
  • .NET 7.0以降が公式サポート
  • 制限がそこそこある
    • リフレクションとかと相性悪い
      • そりゃそう
    • .NET7だとGUIに対応してない[ウソ]
      • これはMAUIとかの話。Avaloniaでいいじゃん。
    • モバイル系は.NET 8でも実験的サポート
  • 事前コンパイルなのでプラットホーム依存
  • クロスコンパイルできない
    • まあそりゃそう
      • 中間コードのメリットを捨てて最適化する方法だしね
    • x64 - arm64はできることもあるっぽい

OptimizationPreference

  • サイズと速度どちら優先かを選べる
<OptimizationPreference>Size</OptimizationPreference>
<OptimizationPreference>Speed</OptimizationPreference>

IlcInstructionSet / IlcMaxVectorTBitWidth

.NET 8.0 >=
<PropertyGroup>
  <IlcInstructionSet>native</IlcInstructionSet>
  <IlcMaxVectorTBitWidth>512</IlcMaxVectorTBitWidth>
</PropertyGroup>

https://zenn.dev/naminodarie/articles/dotnet_native_aot_i

Dynamic PGO

  • .NET 8.0以降 そのままで有効
  • .NET 7.0
<PropertyGroup>
    <TieredPGO>true</TieredPGO>
</PropertyGroup>
いぬいぬいぬいぬ

変化の大きいC#の文字列の最適化手法

前提:C#の文字列stringはネックになりがち

  • C#のstringはimmutable
  • 書き換えは新しい文字列が作られる
  • 新しく作られる→ヒープメモリ確保→メモリアロケーション→GCが走りやすくなる
  • …でネックになりやすいらしい

文字列最適化手法の変化

古い情報だと「とりあえずStringBuilder」というものが多いけど、最近は色々最適化されるようになってきていて昔の方法が通じないかんじ。

  • 昔:
    • 「文字列結合は+を使わずStringBuilderで!」
  • .NET 8時代
    • ある程度固定されてる文字列なら文字列補完を使う $"abc{val}"
    • StringBuilderより文字列補完の中で使われてるDefaultInterpolatedStringHandlerの方が速くなる
    • 文字列がUTF-8なら(C#内部はUTF-16)、UTF8文字列リテラル("あいう"u8)で扱う
      • ReadOnlySpan<byte>として扱われる

https://aneuf.hatenablog.com/entry/2023/12/12/000000

https://zenn.dev/ryobee/articles/badad3d7b45ab7

https://neue.cc/2023/10/13_Utf8StringInterpolation.html

https://gitan.dev/?p=320

.NET8時代の考え方

  • 大半は 生文字列リテラル文字列補完 でOK
    • C#は新しい書き方が「速くて便利」になるようにしているらしい
    • 文字列の加工が必要な場面って大半がテンプレ的な文字列だったり
C# 11.0以降
var yatta = "やったぜ!";
var str = $"""
  改行OK! ({yatta})
  文字列エスケープ不要! ({yatta})
  インデントしてもOK! ({yatta})
  """;

https://ufcpp.net/study/csharp/st_string.html?p=2#raw-string

String.Intern はそのままだと自作キャッシュに負けるがNativeAOTで逆転

https://sergeyteplyakov.github.io/Blog/benchmarking/2023/12/10/Intern_or_Not_Intern.html

いぬいぬいぬいぬ

.NET 8.0の高速化用新機能・ライブラリ

  • System.Collections.Frozen
    • 「コレクションの作成後にキーと値を変更することができません。 この要件により、読み取り操作を高速化できます」
    • nugetで導入できる
  • System.Buffers.SearchValues
  • System.Text.CompositeFormat
    • 「コンパイル時に不明な書式指定文字列を最適化する場合に便利」
    • 前のバージョン向けには公開されてない…
  • System.IO.Hashing.XxHash3 / System.IO.Hashing.XxHash128
  • System.Runtime.Intrinsics.Vector512<T>
    • AVX-512(SIMD)
    • 前のバージョン向けには公開されてない…
  • Parallel.ForAsync
いぬいぬいぬいぬ

決して最速じゃない配列T[]

  • C#の配列T[]は利便性なら List<T>, パフォーマンスなら Span<T>に負ける(ことがある)
    • .NET 8.0時代だと、なんでもかんでも配列化、はNG

  • C#の配列はプリミティブなものだから速い、というわけでもない
    • 要素の追加削除で再割り当て
    • コピーが発生(メモリアロケーション)
    • .NETの内部ではT[] -> Span<T>への置き換えが進んでいて高速化が図られている

https://qiita.com/Kujiro/items/21d004bc74449765dbf7

  • バッファで使うならnew byte[]じゃなくて
    • サイズが小さいならstackalloc byte[]Span<T>/ReadOnlySpan<T>で扱う
    • 大きいorわからないなら ArrayPool<T>.Shared.Rent()

https://ikorin2.hatenablog.jp/entry/2020/07/25/113904

https://ufcpp.net/study/csharp/resource/span/#safe-stackalloc

いぬいぬいぬいぬ

TaskとValueTask, TupleとValueTuple

  • Valueがついてる方が構造体
    • パフォーマンスを意識した実装

TaskとValueTask

  • Task<T>の方が古い
    • クラスなのでムダが…
    • awaitしないで抜ける時とか割とよくある
  • ValueTask<T>
    • 理由がなければこれで

TupleとValueTuple

  • ValueTupleは直接使うことない
    • タプル記法の中身がこれ
  • Tuple型はクラスでメモリアロケーション
    • まあつかわないかな(匿名型も)
いぬいぬいぬいぬ

インライン化阻害防止

反復・例外処理をprivate/local関数化

C# コンパイル結果の IL 命令が32バイトを超える場合、インライン化しない
反復処理を含む場合、インライン化しない
例外処理を含む場合、インライン化しない

ので

反復(forとか)や例外のところだけstaticな(privateやローカル)関数で外に出すとインライン化されやすくなる。

https://ufcpp.net/study/csharp/structured/miscinlining/#inlining-perf

インライン化属性

[MethodImpl(MethodImplOptions.AggressiveInlining)]を付与すると、サイズが小さければインライン化してくれる(こともある)。

インライン化防止属性([MethodImpl(MethodImplOptions.NoInlining)])はベンチマーク取る時に大事。

インライン化すれば必ず早くなるわけじゃないのに注意。
DynamicPGOはその辺動的に判断するらしい。

例外処理用属性

DoesNotReturn
DoesNotReturnIf

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/attributes/nullable-analysis#stop-nullable-analysis-when-called-method-throws

PolySharpで古い環境にも入れられる

ThrowHelper/ スローヘルパー 系

System.ThrowHelper

  • dotnet内部で使われてる
  • internal :cry:
  • コードサイズ削減が目的

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs

https://dotnetdocs.ir/Post/63/-use-throwhelper-instead-of-throw-new-exception

ThrowIf~

.NET には、特定の条件 (ArgumentNullException.ThrowIfNull と ArgumentException.ThrowIfNullOrEmpty) で例外をスローするヘルパー メソッドも用意されています。

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/statements/exception-handling-statements

一部はExceptionクラスのメソッドとして利用できる

Microsoft.Toolkit.Diagnostics.ThrowHelper

  • .NET Community Toolkit に例外を外部関数化したThrowHelperがライブラリ化されている.
  • コードサイズも小さくなる。
  • Guard API

https://learn.microsoft.com/ja-jp/dotnet/communitytoolkit/diagnostics/throwhelper

https://learn.microsoft.com/ja-jp/dotnet/api/communitytoolkit.diagnostics.throwhelper

Guard / ガード 系

いわゆる「ガード句」用のライブラリも同じ用途で使える。

CommunityToolkit.Diagnostics.Guard

public static void SampleMethod(int[] array, int index, Span<int> span, string text)
{
    Guard.IsNotNull(array);
    Guard.HasSizeGreaterThanOrEqualTo(array, 10);
    Guard.IsInRangeFor(index, array);
    Guard.HasSizeLessThanOrEqualTo(array, span);
    Guard.IsNotNullOrEmpty(text);
}

https://learn.microsoft.com/ja-jp/dotnet/communitytoolkit/diagnostics/guard

https://learn.microsoft.com/ja-jp/dotnet/api/communitytoolkit.diagnostics.guard

いぬいぬいぬいぬ

LINQは遅い?

決して最速じゃない配列T[]」で書いたようにfor文でT[]を色々するよりLINQが早い場合もある。

新しい.NETを使う

  • SIMD化などで高速化するものがある
    • .NET 7.0
      • int/longのMin() / Max()
    • .NET 8.0
      • int/long以外のMin() / Max()
      • Sum()
      • Order() / OrderDescending()
      • keyが値型の時の OrderBy() / OrderByDescending()
      • new Dictionary<K, V>(List<KeyValuePair<K, V>>)

PLINQで並列処理する

  • データ量がある程度多いならforで回すよりPLINQで並列化のほうが効果的
    • Parallel.For とかでもいいけど…

LINQ高速化ライブラリを使う

間違った使い方をしない

  • Enumerableで長さを確認するのにAny()を使わない

LINQ 高速化ライブラリ比較

https://github.com/NetFabric/LinqBenchmarks

  • .NET8.0ではちょくちょく素のLINQが最速というパターンがある
  • 単純なfor/foreachより早いケースも
いぬいぬいぬいぬ

プロファイル

ベンチマーク

BenchmarkDotNet

benchmark.net使用時注意

  1. 複数ランタイムで比較する
    • .NET Frameworkと.NETの最新LTS、最新とか
    • 内部処理が違うので同じコードでも差がでる
    • 可能ならR2R・NativeAOTも比較
      • 最新版だけでいいかも
  2. インライン化を抑制する
    • 純粋な処理の差を見るため
    • [MethodImpl(MethodImplOptions.NoInlining)]
  3. Dynamic PGOを抑制する
    • 純粋な処理の差を見るため
    • .NET 8.0~

アプリprofiler

いぬいぬいぬいぬ

ローカルstaticフィールドはReadOnlySpan<T>+コレクション式でできる

void DoSomething(){
   //できない
   //static int[] arr = {1,2,3};
   
   //staticと同じ扱い
   ReadOnlySpan<int> arr = [1,2,3];
}
  • ローカルなReadOnlySpan<T>変数をコレクション式で宣言するとstaticと同等になる

  • 読み取りが高速化

    • メモリは減らない
  • sharplabで調べたら本当だった

    • 現時点だと__StaticArrayInitTypeSize=12_Align=4っていう自動生成されたクラス内のstaticな構造体になってる

https://sharplab.io/#v2:D4AQTAjAsAUCDMACciDCiDetE+UkALIgLIAUAlJtrjQPS0gQBsiAlgHYAuA2gLqIBDAE5DEAXkwQANGCnwAvgG5qNHACUApgIAmAeXYAbAJ4BlAA4D2AHg6cAfIJHjE3abPi9lMVbhWrGAJykwkIAdAAqAPYmnEIcAOYU5F408rDyQA=

いぬいぬいぬいぬ

newしない初期値・リセット

  • 初期化・リセットのたびにnewすると遅い
  • 代わりに空を示すプロパティ・メソッドが用意されてる
  • わかりやすくなるメリットも

文字列 string.Empty

  • ""の代わり
  • 現状だとパフォーマンス面であんまり差はないかも?
    • わかりやすさは上

https://learn.microsoft.com/ja-jp/dotnet/api/system.string.empty?view=net-8.0

配列 Array.Empty<T>()

  • メモリアロケーション防止

コレクション Enumerable.Empty<T>()

  • メモリアロケーション防止
  • .NET8時点ではEmptyPartition<T>.Instanceっていうキャッシュされたゼロenumberableが返ってくるっぽい

https://github.com/dotnet/runtime/blob/a6e4834d53ac591a4b3d4a213a8928ad685f7ad8/src/libraries/System.Linq/src/System/Linq/Partition.SpeedOpt.cs#L20-L25

リスト ImmutableArray<T>.Empty

  • List<T>.Emptyはない
  • けど、ImmutableArray<T>.Emptyならある

https://learn.microsoft.com/ja-jp/dotnet/api/system.collections.immutable.immutablearray-1.empty?view=net-8.0

  • ReadOnlyCollectionにもある
    • .NET8以降

https://learn.microsoft.com/ja-jp/dotnet/api/system.collections.objectmodel.readonlycollection-1.empty?view=net-8.0

  • FrozenSet / FrozenDictionaryにもある

Span<T> Span<T>.Empty

  • Span<T>, ReadOnlySpan<T>, Memory<T>, ReadOnlyMemory<T>にある

コレクション式で初期化 []

.NET8.0時点では、ケースバイケース。

  • T[]
    • Array.Empty<T>()が呼ばれる 👍
  • List<T>
    • new List<T>()と同じ
  • ImmutableArray<T>
    • ImmutableCollectionsMarshal.AsImmutableArray(Array.Empty<T>())が呼ばれる
  • Span<T>
    • default(Span<T>)と同じ
  • string
    • 使用不可
  • UTF-8 string
    • default(ReadOnlySpan<byte>)と同じ
int[] array = [];  //Array.Empty<int>();
List<int> list = []; //new List<int>();
ImmutableArray<int> im = []; //ImmutableCollectionsMarshal.AsImmutableArray(Array.Empty<int>());
Span<int> span = []; //default(Span<int>);
//string str = [];  //error!
ReadOnlySpan<byte> u8str = []; //default(ReadOnlySpan<byte>);

https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA+ABATARgLABQGADAAQY4oDchJ5OAdAMIQA2rMYALgJYQB2AZwYBJALZiArlwCGwDjSJkKzNh258hDAPLAAVpy4BZCABMYrRXRUARHjIDm/CIN5hBV5YwAyPfgEcrAGZyLFImUgBvQlJY8hCMFFIRfh4uAEEoKBkATwAKAEoomLjSvy4AbQBdUhks3NIAXiiAX0VSsv5KmrrsnLDm6vaO2JKRigBOPN6GxuaZ/oLh2Jax0jWMBKSUtIBlLig/B0LighHYijJXKCbSACI75Y6AemfL0muB0iHSV5gs6DhXY4ADsWAmIFIAHJLlDSKYIDBBKRnFxSJAujI/LV0QJrpJuICuAALGRo2QAayRpDIdQckjEMC6gjWpVekzy1yazU+S1ZcX5sQASjAZKZtPxWDldgAHGT8AA8wByXBgAD5SJIABxc5oPbVPUoisUSqWy+VKlXqzU6g5fIaC0jG8WS6VyxXK1Ua7XXELNZ2mt0Wz3qhgAUTEMq4OUNcQ5ABI7pEfQcGN4mQ4SS0ADTJ21QLBpjNZu58s5xVbl0ZV+LkbapLjm/gnaI10pNhXlDWCd23fgwADupHKFWIVUibUdHa7H3dADlB33B4VY7Fp11u+77VVV6R11xN/K/Xv3Z2N+HI9Hd46E0me/Ki/xM8Sc5F7/wFwPH8/X+/C+mnyzXN3yCb8SzLUpK1KDYtmSBtfFcFtHQQrgzwPUhWB4VwlwHFdkKw1CZ0w1xt13FC0I1YiuGPMN+AZf45A4C8oxyCjCgYAAVCAULwtsBT4i4cCmKjuQwgisAgis1hgus4LScQpFkeQYEyPokIE5IJGkRiVPqViZx4MQcN485NMUnTVNyCjhzEUjHQU7TlMs/SNxs48HKUjhnIo5irxvIS8kTSJDNfQysFCsQghaUsnig/jSk2WSdi4IVtBYdhDE0dTTIDV10vUXgBGswzjOqSSRleXKpXyzKioM2zbgdDSqpyGqNDq1zDOPFq2sKxUu18mN/KmIKQtzMbgsi6LytISsWiAA==

いぬいぬいぬいぬ

半公式の高速化ライブラリ

.NET Community Toolkit

  • コミュニティベースのライブラリだが、MSが開発していてMS Learnにリファレンスがある
  • 以下がパフォーマンスに影響
    • CommunityToolkit.Diagnostics
    • CommunityToolkit.HighPerformance
      • Span2D<T> / Memory2D<T>
      • MemoryOwner<T> / SpanOwner<T>
      • StringPool
      • ParallelHelper

https://learn.microsoft.com/ja-jp/dotnet/communitytoolkit/diagnostics/introduction
https://learn.microsoft.com/ja-jp/dotnet/communitytoolkit/high-performance/introduction

.NEXT

.NEXT (dotNext) is the family of powerful libraries aimed to improve development productivity and extend the .NET API with unique features. The project is supported by the .NET Foundation.

  • dotnet公式リポジトリで管理されている

https://dotnet.github.io/dotNext/index.html
https://qiita.com/up-hash/items/059975d06347b81087de

Microsoft.IO.RecyclableMemoryStream

System.IO.MemoryStreamに特化したプーリングライブラリ

https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream

https://synamon.hatenablog.com/entry/2023/04/24/000000

いぬいぬいぬいぬ

実行時最適化

Tiered Compilation

Tiered Compilation

P18

  • 速度優先(QuickJIT, Tier 0)と最適化優先(Tier 1)に"実行時に"処理が分かれる
    • .NET Core 3.0以降
  • csproj or runtimeconfig.json or 環境変数で設定変更可能
  • R2RでビルドするとTier1に
<TieredCompilation>false</TieredCompilation>
<TieredCompilationQuickJit>false</TieredCompilationQuickJit>
<TieredCompilationQuickJitForLoops>true</TieredCompilationQuickJitForLoops>

On-Stack Replacement

情報

https://x.com/nenoMake/status/1748976523686887910?s=20

.NET 8 で既定で有効になった Dynamic PGO について - Speaker Deck
.NET の最適化の罠? (インライン展開がされなかった理由) #C# - Qiita