Spanned是一種高性能的零分配.NET庫,它引入了流行的BCL類型的跨度兼容替代品,並為跨越跨度的常見操作提供了矢量化解決方案。
首先,首先將跨度軟件包添加到您的項目中。您可以通過運行以下命令來執行此操作:
dotnet add package Spanned另外,您可以使用此命令通過軟件包管理器控制台安裝它:
Install-Package Spanned請注意,隨著時間的推移,.NET累積了許多依賴框架和運行時的優化路由。因此,對於單個代碼庫中不同框架的鄰居高度優化的代碼,它變得極具挑戰性。因此,我做出了一個艱難的決定,每個版本的每個版本都只支持一個框架。您可以在下表中找到庫版本的列表和相應的支持的框架版本:
| .NET標準2.0 | .NET標準2.1 | .NET 8+ | |
|---|---|---|---|
雖然可能很想使用v0.0.1滿足您的所有需求,但鑑於其對.NET標準2.0的支持以及如今的.NET標準2.0的廣泛採用,但我強烈建議您不要這樣做。幾乎沒有優化可以使用此舊框架執行。即使是我們心愛的Span ,也可以通過System.Memory在.NET標準2.0中訪問。 MEMORYNUGET軟件包也被稱為“慢跨度”,因為該Span無非是重新發明的ArraySegment ,在運行時/jit端缺乏適當的支持。因此,請選擇適合您的環境的最佳包裝版本,而不是似乎適合所有環境的版本。
在介紹細節之前,讓我們討論您在使用此庫時可能會遇到的一些常見邊緣案例,然後回答以下問題:“為什麼X的一部分是.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 ,因此原始的ValueStringBuilder不會意識到這一點,並且將繼續使用已經處置的緩衝區。
儘管這種方法似乎可以在測試過程中使用消毒的輸入,但偶爾會失敗,不僅會破壞您的代碼,還會破壞應用程序運行時的某些隨機部分,即通過乾擾一個緩衝區,即ValueStringBuilder的副本已經返回池,因此可以由其他東西重複使用。
您可能會嘗試通過重寫有問題的擴展方法來解決問題,並解決問題:
public static void AppendUserName ( this in ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
}現在,通過引用通過ValueStringBuilder ,所以應該沒有問題,對嗎?好吧,不。 in修改器的存在是為了通過傳遞對方法的引用,同時保存語義,就好像按值通過值,從而降低了價值類型實例的整體成本。這意味著所提供的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 ) ;
}雖然不像某些人想要的那樣幻想,但該解決方案具有實際工作的好處。
本庫提供的大多數類型定義了一個Dispose()方法,使其與using關鍵字一起使用,如下所示:
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 | 比率 | 代碼大小 |
|---|---|---|---|---|
| 沒有_spanowner_int32 | 5.134 ns | 0.0425 ns | 1.00 | 315 b |
| 使用_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 s,則表現可能會更好。這就是為什麼存在.UnsafeMin()和.UnsafeMax() 。這些方法特定於包含浮點數的跨度,並且它們執行比較操作而不承認NaN的存在,從而消除了所有相關檢查。因此,如果您絕對確定會進行跨越的浮點數,並且不能包含任何NaN ,則可以從.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()的兩倍,比計算循環中的總和快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() is an extension method that can help you compute the sum of all values in a span using a 64-bit accumulator (ie, long for signed integers, ulong for unsigned integers, and double for float ) , capable of storing a result larger than the maximum/minimum value of the original type (eg, you cannot store the mathematically correct result of int.MaxValue + int.MaxValue in a variable of type int ) 。它是所有受支持類型的矢量化,並且在LINQ中沒有適當的選擇(因此,下面的基準有點不公平) 。
.LongSum()沒有“不安全”的對應物,因為即使是存儲int類型的int.maxvalue元素的最大跨度也不會導致64位累加器的溢出( ((long)int.maxvalue *(long) int.MaxValue (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()是一種擴展方法,可以幫助您計算跨度中所有值的平均值。它是所有受支持類型的矢量化的,與Enumerable.Average()不同,它僅為32位簽名的整數(即IE, int S)提供一定程度的優化。
.LongSum()引擎蓋下.Average()但是,如果您的輸入被消毒並且不能導致一個,則可以切換到使用.UnsafeAverage()的.unsafeaverage .UnsafeSum() ,並且不會將寶貴的執行時間花在溢出後衛上。
| 方法 | 意思是 | stddev | 比率 | 代碼大小 |
|---|---|---|---|---|
| 平均_loop_int16 | 2,482.1 ns | 20.04 ns | 1.00 | 241 b |
| 平均_linq_int16 | 13,198.2 ns | 97.67 ns | 5.31 | 1,016 b |
| 平均_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 | 比率 | 代碼大小 |
|---|---|---|---|---|
| 填充sequential_loop_int16 | 2,499.4 ns | 28.47 ns | 1.00 | 118 b |
| 填充sequential_span_int16 | 169.2 ns | 0.18 ns | 0.07 | 660 b |
.IndexOf() ,. .LastIndexOf()和.Contains()似乎對您來說似乎很熟悉,因為這些方法由MemoryExtensions啟用提供。但是,它們有兩個問題:
IEquatable<T> ,這使得它們在無限的通用上下文中無法訪問。IEqualityComparer<T>實現。此庫提供的這些方法的實現都解決了這兩個問題。
根據MIT許可條款許可。