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.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许可条款许可。