Spanned 、人気のあるBCLタイプのスパン互換性のある代替品を導入し、スパンで共通操作のためのベクトル化されたソリューションを提供する、高性能のゼロアロケーション.NETライブラリです。
開始するには、最初にプロジェクトにスパンパッケージを追加します。次のコマンドを実行することでこれを行うことができます。
dotnet add package Spannedまたは、このコマンドを使用して、パッケージマネージャーコンソールを介してインストールすることもできます。
Install-Package Spanned.NETは、フレームワーク依存とランタイム依存の両方で、時間の経過とともに多数の最適化ルートを蓄積していることに注意してください。その結果、単一のコードベースの異なるフレームワークに対して、近隣の高度に最適化されたコードにとって非常に挑戦的になりました。したがって、私はこのライブラリのバージョンごとに1つのフレームワークのみをサポートすることを難しい決定を下しました。以下の表に、ライブラリバージョンのリストと対応するサポートされているフレームワークバージョンを見つけることができます。
| .NET標準2.0 | .NET標準2.1 | .NET 8+ | |
|---|---|---|---|
.NET Standard 2.0のサポートと最近の.NET Standard 2.0の広範な採用を考えると、すべてのニーズにv0.0.1使用することは魅力的かもしれませんが、そうすることに強くお勧めします。このレガシーフレームワークを使用して実行できる最適化は事実上ありません。 System.Memory Nugetパッケージを介して.NET標準2.0にアクセスできる最愛のSpanでさえ、「スロースパン」として知られています。そのSpan 、ランタイム/JIT側の適切なサポートがないため、再発明されたArraySegmentにすぎないためです。したがって、環境に最適なパッケージバージョンを選択してください。すべてに合っていると思われるものではありません。
詳細に入る前に、このライブラリを使用している際に遭遇する可能性のある一般的なエッジケースについて話し合い、「なぜ.NETの一部ではないのか」という質問に答えましょう。要するに、ここで見つけることができるものはすべて、使いやすく、誤用しやすいものです。
明らかなポイントから始めましょう。このライブラリは、すべての割り当てられたバイトと、非常に重要なパスでの実行時間のすべてのナノ秒のために、歯と爪と戦うシナリオ向けに特別に設計されています。それは、コードベース全体の万能ソリューションであることを意図したものではありません。考えられるあらゆるシナリオにこのライブラリからタイプを無意識に組み込むことで、アプリケーションを強化するのではなく、アプリケーションの全体的なパフォーマンスを低下させる可能性があります。
最初にナノ最適化の海に向かって、それが必要になるまで頭を飛び込まないでください。
避けるべき一般的な間違いは、このライブラリが提供するタイプを価値によって渡すことです(ええ、これだけでは、このようなものがBCLの一部になるべきではなく、決してない理由を理解するのに役立つはずです) 。たとえば、次のコードは正常に見えるかもしれませんが、実際には悲惨です。
ValueStringBuilder sb = new ValueStringBuilder ( 16 ) ;
sb . AppendUserName ( user ) ;
// ...
public static void AppendUserName ( this ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
}文字列ビルダーに追加すると、内部バッファーを拡大する場合があります。ただし、 ValueStringBuilder Value (つまり、コピーした)で渡したため、元のValueStringBuilderそれを認識せず、既に廃棄されたバッファーを引き続き使用します。
このアプローチは、テスト中に消毒された入力で動作するように見えるかもしれませんが、時々失敗し、コードだけでなく、 ValueStringBuilderのコピーがすでにプールに戻っているバッファーを妨害することにより、アプリのランタイムのランダムな部分を破壊します。
次のように、問題のある拡張法を書き直して問題に賢くなり、問題に対処しようとするかもしれません。
public static void AppendUserName ( this in ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
}さて、 ValueStringBuilder参照によって渡されるので、問題はないはずですよね?まあ、いいえ。 in Modifierは、セマンティクスを値で渡されたかのように保存しながら、メソッドに基準を渡すことにより、値タイプインスタンス全体をコピーするコストを削減するために存在します。これは、提供されたValueStringBuilderの状態の変更が元のインスタンスに伝播されないことを意味します。だから、私たちはまだ私たちの手に同じ問題を抱えています。値タイプインスタンスの内部状態を変更できるメソッドを実装する唯一の正しい方法は、実際に参照によって渡すことです。
ValueStringBuilder sb = new ValueStringBuilder ( 16 ) ;
AppendUserName ( ref sb , user ) ;
// ...
public static void AppendUserName ( ref ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
}一部の人が望んでいるほど派手ではありませんが、このソリューションは実際に機能するという利点があります。
このライブラリが提供するタイプのほとんどは、以下に示すように、 usingキーワードで使用できるようにDispose()メソッドを定義しています。
using ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
return sb . ToString ( ) ;ただし、これは、 usingキーワードで使用する必要があるという意味ではありません。上記のコードが実際にどのように低下したかを覚えておくことは非常に重要です。
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
try
{
Foo ( ref sb ) ;
return sb . ToString ( ) ;
}
finally
{
sb . Dispose ( ) ;
}保護された地域の作成と管理は無料ではありません。ナノ最適化への焦点を考慮すると、ここでの影響は顕著です。したがって、 Dispose()手動で呼び出すことが望ましいです。
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
string result = sb . ToString ( ) ;
sb . Dispose ( ) ;
return result ;または、特定のタイプで呼び出す最後のメソッドに自動的にクリーンアップを実行するオーバーロードがあるかどうかを確認します。
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
return sb . ToString ( dispose : true ) ;現代の.NETでは、次のパターンに遭遇することが一般的です。
または、コードで同じ概念を表現する:
const int StackAllocByteLimit = 1024 ;
T [ ] ? spanSource ;
scoped Span < T > span ;
if ( sizeof ( T ) * length > StackAllocByteLimit )
{
spanSource = ArrayPool < T > . Shared . Rent ( length ) ;
span = spanSource . AsSpan ( 0 , length ) ;
}
else
{
spanSource = null ;
span = stackalloc T [ length ] ;
}
DoSomeWorkWithSpan ( span ) ;
if ( spanSource is not null )
{
ArrayPool < T > . Shared . Return ( spanSource ) ;
}ボイラープレートの最もきれいな部分ではありませんか?実際のロジックはしばしばそれによって埋もれてしまいますが、それは理想とはほど遠いものです。これは、 SpanOwner解決することを目指している正確な問題です。これは同じロジックですが、すべてのボイラープレートがSpanOwner背後に隠されています。
SpanOwner < T > owner = SpanOwner < T > . ShouldRent ( length ) ? SpanOwner < T > . Rent ( length ) : stackalloc T [ length ] ;
Span < T > span = owner . Span ;
DoSomeWorkWithSpan ( span ) ;
owner . Dispose ( ) ;書くのがはるかに簡単で、読みやすくなり、最も重要なことに、このアプローチは、 SpanOwner完全にインラインできるように設計されているため、まったく同じパフォーマンスを提供します。 JITによってコードから完全に排除できます。
| 方法 | 平均 | stddev | 比率 | コードサイズ |
|---|---|---|---|---|
| withe_spanowner_int32 | 5.134 ns | 0.0425 ns | 1.00 | 315 b |
| with_spanowner_int32 | 4.908 ns | 0.0168 ns | 0.96 | 310 b |
ValueStringBuilder 、スタックに割り当てられたバッファーをサポートするように設計されたStringBuilderの再実装です。共有アレイプールを使用して、必要に応じて内部バッファーを拡張することができます。 ValueStringBuilder 、スタックに収まるコンパクトな文字列を構築するのに最適です。ただし、より大きな文字列の操作がアプリの全体的なパフォーマンスを低下させる可能性があるため、他のものには使用するべきではありません。 ValueStringBuilderの真の輝きは、ストリングとしてまったく具体化する必要のない短い文字シーケンスを作成する必要がある場合に現れます。
ValueStringBuilder 、 StringBuilderのすべての機能をミラーリングし、同じユニットテストのセットを正常に渡すため、ほとんどのシナリオでドロップイン交換としてシームレスに機能します。
// Note that providing a capacity instead of a buffer will force
// the builder to rent an array from `ArrayPool<char>.Shared`.
ValueStringBuilder sb = new ( stackalloc char [ 256 ] ) ;
// `ValueStringBuilder` provides a custom interpolated string handler,
// ensuring such operations do not allocate any new strings.
sb . Append ( $ "Hello, { user . Name } ! Your ID is: { user . id } " ) ;
// Unlike `StringBuilder`, `ValueStringBuilder` can be represented
// as a readonly span. Thus, you don't need to actually materialize
// the string you've built in lots of cases.
DisplayWelcome ( ( ReadOnlySpan < char > ) sb ) ;
// Remember to dispose of the builder to return
// a rented buffer, if any, back to the pool.
sb . Dispose ( ) ; ValueList<T>スタックに割り当てられたバッファーをサポートするように設計されたList<T>の再実装です。共有アレイプールを使用して、必要に応じて内部バッファーを拡張することができます。 ValueList<T> 、スタックに適合できる少量のデータを処理するのに最適です。ただし、より大きなデータセットでの操作は、アプリの全体的なパフォーマンスを低下させる可能性があるため、他のものに使用するべきではありません。
ValueList<T> 、 List<T>のすべての機能をミラーリングし、同じ単位テストのセットを正常に渡すため、ほとんどのシナリオでドロップイン交換としてシームレスに機能します。
// Note that providing a capacity instead of a buffer will force
// the list to rent an array from `ArrayPool<T>.Shared`.
ValueList < int > list = new ( stackalloc int [ 10 ] ) ;
list . Add ( 0 ) ;
list . Add ( 1 ) ;
list . Add ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) list ) ;
// Remember to dispose of the list to return
// a rented buffer, if any, back to the pool.
list . Dispose ( ) ; ValueStack<T>スタックに割り当てられたバッファーをサポートするように設計されたStack<T>の再実装です。共有アレイプールを使用して、必要に応じて内部バッファーを拡張することができます。 ValueStack<T>スタックに適合できる少量のデータを処理するのに最適です。ただし、より大きなデータセットでの操作は、アプリの全体的なパフォーマンスを低下させる可能性があるため、他のものに使用するべきではありません。
ValueStack<T> 、 Stack<T>のすべての機能をミラーリングし、同じ単体テストのセットを正常に渡すため、ほとんどのシナリオでドロップインの交換をシームレスに機能させることができます。
// Note that providing a capacity instead of a buffer will force
// the stack to rent an array from `ArrayPool<T>.Shared`.
ValueStack < int > stack = new ( stackalloc int [ 10 ] ) ;
stack . Push ( 0 ) ;
stack . Push ( 1 ) ;
stack . Push ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) stack ) ;
// Remember to dispose of the stack to return
// a rented buffer, if any, back to the pool.
stack . Dispose ( ) ; ValueQueue<T>は、スタックに割り当てられたバッファーをサポートするように設計されたQueue<T>の再実装です。共有アレイプールを使用して、必要に応じて内部バッファーを拡張することができます。 ValueQueue<T> 、スタックに適合できる少量のデータを処理するのに最適です。ただし、より大きなデータセットでの操作は、アプリの全体的なパフォーマンスを低下させる可能性があるため、他のものに使用するべきではありません。
ValueQueue<T> 、 Queue<T>のすべての機能をミラーリングし、同じユニットテストのセットを正常に渡すため、ほとんどのシナリオでドロップインの交換をシームレスに機能させることができます。
// Note that providing a capacity instead of a buffer will force
// the queue to rent an array from `ArrayPool<T>.Shared`.
ValueQueue < int > queue = new ( stackalloc int [ 10 ] ) ;
queue . Enqueue ( 0 ) ;
queue . Enqueue ( 1 ) ;
queue . Enqueue ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) queue ) ;
// Remember to dispose of the queue to return
// a rented buffer, if any, back to the pool.
queue . Dispose ( ) ; ValueSet<T>スタックに割り当てられたバッファーをサポートするように設計されたHashSet<T>の再実装です。共有アレイプールを使用して、必要に応じて内部バッファーを拡張することができます。 ValueSet<T> 、スタックに適合できる少量のデータを処理するのに最適です。ただし、より大きなデータセットでの操作は、アプリの全体的なパフォーマンスを低下させる可能性があるため、他のものに使用するべきではありません。
ValueSet<T> 、 HashSet<T>のすべての機能をミラーリングし、同じユニットテストのセットを正常に渡すため、ほとんどのシナリオでドロップインの交換をシームレスに機能させることができます。
// Note that providing a capacity instead of a buffer will force
// the set to rent an array from `ArrayPool<T>.Shared`.
ValueSet < int > set = new ( stackalloc int [ 10 ] ) ;
set . Add ( 0 ) ;
set . Add ( 1 ) ;
set . Add ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) set ) ;
// Remember to dispose of the set to return
// a rented buffer, if any, back to the pool.
set . Dispose ( ) ; ValueDictionary<TKey, TValue> Dictionary<TKey, TValue>の再実装です。共有アレイプールを使用して、必要に応じて内部バッファーを拡張することができます。 ValueDictionary<TKey, TValue>スタックに適合できる少量のデータの処理に最適です。ただし、より大きなデータセットでの操作は、アプリの全体的なパフォーマンスを低下させる可能性があるため、他のものに使用するべきではありません。
ValueDictionary<TKey, TValue> Dictionary<TKey, TValue>のすべての機能をミラーリングし、同じ単位テストのセットを正常に渡すため、ほとんどのシナリオでドロップインの交換をシームレスに機能させることができます。
// Note that providing a capacity instead of a buffer will force
// the dictionary to rent an array from `ArrayPool<T>.Shared`.
ValueDictionary < int , string > dictionary = new ( 10 ) ;
dictionary . Add ( 0 , "zero" ) ;
dictionary . Add ( 1 , "one" ) ;
dictionary . Add ( 2 , "two" ) ;
DoSomethingWithPairs ( ( ReadOnlySpan < KeyValuePair < int , string > > ) dictionary ) ;
// Remember to dispose of the dictionary to return
// a rented buffer, if any, back to the pool.
dictionary . Dispose ( ) ; .Min()および.Max()は、スパンで最小値/最大値を見つけるのに役立つ拡張メソッドです。それらは、 Enumerable.Min()およびEnumerable.Max()とは異なり、サポートされているすべてのタイプに対してベクトル化されており、浮動小数点数の最適化を提供しません。
ただし、浮動小数点数(すなわち、 float 、 double )にはわずかな問題があり、この問題の名前はNaNです。ご存知かもしれませんが、 NaNはどの数よりも大きくも少なく、それ自体にも任意の数に等しくありません。したがって、 NaNが提供されたシーケンスに存在する場合、通常の比較操作の結果にのみ依存する素朴な実装を混乱させる可能性があります。したがって、 NaNが提供されたシーケンスに存在する場合、通常の比較操作の結果にのみ依存する素朴な実装を混乱させる可能性があります。したがって、この浮動小数点比較の悩みを考慮していないことはオプションではありません。
Spanned 、すべてのNaN関連のチェックを非常に効率的な方法で採用し、最適化されていないソリューションを大幅に向上させることができます。ただし、 NaNを説明する必要がなければ、パフォーマンスはさらに優れている可能性があります。これが.UnsafeMin()および.UnsafeMax()存在する理由です。これらの方法は、浮動小数点数を含むスパンに固有であり、 NaNの存在を認めずに比較操作を実行し、関連するすべてのチェックを排除します。したがって、浮動小数点数のスパンが消毒され、 NaN sを含めることができないことを絶対に確信している場合、 .Min()および.Max()操作からさらに多くのパフォーマンスを絞ることができます。
.Min()と.UnsafeMin()の違いはあまり目立たないかもしれませんが、
| 方法 | 平均 | stddev | 比率 | コードサイズ |
|---|---|---|---|---|
| min_loop_single | 3,919.5 ns | 15.75 ns | 1.00 | 207 b |
| min_linq_single | 4,030.3 ns | 37.38 ns | 1.03 | 570 b |
| min_span_single | 611.1 ns | 8.55 ns | 0.16 | 534 b |
| unsafemin_span_single | 569.0 ns | 1.82 ns | 0.15 | 319 b |
パフォーマンスのギャップは.Max()と.UnsafeMax()の間で非常に重要になります。
| 方法 | 平均 | stddev | 比率 | コードサイズ |
|---|---|---|---|---|
| max_loop_single | 3,849.2 ns | 36.97 ns | 1.00 | 215 b |
| max_linq_single | 3,936.4 ns | 53.51 ns | 1.02 | 643 b |
| max_span_single | 901.7 ns | 7.12 ns | 0.23 | 606 b |
| unsafemax_span_single | 551.8 ns | 3.06 ns | 0.14 | 321 b |
.Sum()は、スパン内のすべての値の合計を計算するのに役立つ拡張メソッドです。 Enumerable.Sum()とは異なり、すべてのサポートされているタイプに対してベクトル化されています。これは、ベクトル化を欠いているだけでなく、ほとんどの数値タイプの過負荷を箱から出してもまったく提供しません。
.Min()および.Max()と同様に、 .Sum()には、 .UnsafeSum()という名前で行われる邪悪な双子があります。合計計算が整数のオーバーフロー/アンダーフローをもたらす場合、ベースメソッドはOverflowExceptionをスローします。もちろん、オーバーフローガードは犠牲を払っていますが、それは無視できるものではありません。したがって、入力が消毒され、オーバーフローを引き起こすことができない場合、または整数オーバーフローが作業コンテキストで予想される動作である場合、 .UnsafeSum()自由に使用してください。それは.Sum()の2倍の速さで、ループ内の合計の計算よりも34倍高速で、LINQを介して合計を計算するよりも130倍高速です。
| 方法 | 平均 | stddev | 比率 | コードサイズ |
|---|---|---|---|---|
| sum_loop_int16 | 3,820.0 ns | 7.04 ns | 1.00 | 128 b |
| sum_linq_int16 | 14,472.6 ns | 281.83 ns | 3.80 | 732 b |
| sum_span_int16 | 214.6 ns | 2.43 ns | 0.06 | 413 b |
| unsafesum_span_int16 | 111.8 ns | 1.00 ns | 0.03 | 200 b |
.LongSum() 64ビットアキュムレータを使用してすべての値の合計をスパンで計算するのに役立つ拡張メソッドです(つまり、署名された整数の場合はlong 、署名ulong float doubleいない整数の場合は2倍int.MaxValue + int.MaxValueフロートの場合int 2倍)。サポートされているすべてのタイプに対してベクトル化されており、LINQには適切な選択肢がありません(したがって、以下のベンチマークは少し不公平です) 。
.LongSum()は「安全でない」カウンターパートはありません。なぜなら、 intのint.MaxValue要素を蓄積する可能性のある最大のスパンでさえ、64ビットアキュムレータ( (long)int.MaxValue * (long)int.MaxValue < long.MaxValue )のオーバーフローを引き起こすことはできないためです。
| 方法 | 平均 | stddev | 比率 | コードサイズ |
|---|---|---|---|---|
| longsum_loop_int16 | 2,537.1 ns | 21.30 ns | 1.00 | 98 b |
| longsum_linq_int16 | 14,372.0 ns | 130.00 ns | 5.67 | 734 b |
| longsum_span_int16 | 251.0 ns | 2.38 ns | 0.10 | 390 b |
.Average()は、スパン内のすべての値の平均を計算するのに役立つ拡張方法です。 32ビットの署名された整数(つまり、 int )に対してある程度の最適化のみを提供するEnumerable.Average()とは異なり、サポートされているすべてのタイプに対してベクトル化されています。
ボンネットの下で、 .Average()は.LongSum()を使用して、整数のオーバーフローを避けながら、すべての要素の合計を計算します。ただし、入力が消毒され、それを引き起こすことができない場合は、 .UnsafeAverage()を使用し、オーバーフローガードに貴重な実行時間を費やさない.UnsafeSum()に切り替えることができます。
| 方法 | 平均 | stddev | 比率 | コードサイズ |
|---|---|---|---|---|
| Average_loop_int16 | 2,482.1 ns | 20.04 ns | 1.00 | 241 b |
| Average_linq_int16 | 13,198.2 ns | 97.67 ns | 5.31 | 1,016 b |
| Average_span_int16 | 257.8 ns | 3.61 ns | 0.10 | 593 b |
| unsafeaverage_span_int16 | 116.7 ns | 1.27 ns | 0.05 | 128 b |
.FillSequential()は、特定のスパンを連続した数値値で埋めるのに役立つ拡張法です。サポートされているすべてのタイプに対してベクトル化されており、LINQには選択肢がありません。
| 方法 | 平均 | stddev | 比率 | コードサイズ |
|---|---|---|---|---|
| fillsequenty_loop_int16 | 2,499.4 ns | 28.47 ns | 1.00 | 118 b |
| fillsequenty_span_int16 | 169.2 ns | 0.18 ns | 0.07 | 660 b |
.IndexOf() 、. .LastIndexOf() 、および.Contains() 、これらの方法がMemoryExtensionsによって提供されるため、おなじみのように見える場合があります。ただし、それらには2つの問題があります。
IEquatable<T>実装するために入力値を必要とし、それらをUnbound Genericコンテキストでアクセスできなくなります。IEqualityComparer<T>実装をサポートしていません。このライブラリによって提供されるこれらの方法の実装は、これらの問題の両方に対応しています。
MITライセンスの条件に基づいてライセンスされています。