net 5中关于异步的最大变化之一实际上是默认不启用的,但这是另一个获得反馈的实验。net 5中的异步ValueTask池博客更详细地解释,但本质上dotnet/coreclr#26310介绍了异步ValueTask能力和异步ValueTask隐式创建的缓存和重用对象代表一个异步操作完成,使得这些方法amortized-allocation-free的开销。优化目前是可选的,这意味着您需要将DOTNET_SYSTEM_THREADING_POOLASYNCVALUETASKS环境变量设置为1才能启用它。启用这一功能的困难之一是,对于可能要执行比等待SomeValueTaskReturningMethod()更复杂的操作的代码,因为valuetask比任务有更多关于如何使用它们的约束。为了帮助解决这个问题,一种新的UseValueTasksCorrectly分析仪发布了,它将标记大多数此类误用。
ValueTaskCost .NET FW 4.8 1,635.6 us 1.00 294010 B
ValueTaskCost .NET Core 3.1 842.7 us 0.51 120184 B
ValueTaskCost .NET 5.0 812.3 us 0.50 186 B
c#编译器中的一些变化为.NET 5中的异步方法带来了额外的好处(在 .NET5中的核心库是用更新的编译器编译的)。每个异步方法都有一个负责生成和完成返回任务的“生成器”,而c#编译器将生成代码作为异步方法的一部分来使用。避免作为代码的一部分生成结构副本,这可以帮助减少开销,特别是对于async ValueTask方法,其中构建器相对较大(并随着T的增长而增长)。同样来自@benaadams的dotnet/roslyn#45262也调整了相同的生成代码,以更好地发挥前面讨论的JIT的零改进。
在特定的api中也有一些改进。dotnet/runtime#35575诞生于一些特定的任务使用Task.ContinueWith,其中延续纯粹用于记录“先行”任务continue from中的异常。通常情况下,任务不会出错,而PR在这种情况下会做得更好。
Cancel .NET FW 4.8 239.2 ns 1.00 193 B
Cancel .NET Core 3.1 140.3 ns 0.59 192 B
Cancel .NET 5.0 106.4 ns 0.44 112 B
也有一些调整,以帮助特定的架构。由于x86/x64架构采用了强内存模型,当针对x86/x64时,volatile在JIT时基本上就消失了。ARM/ARM64的情况不是这样,它的内存模型较弱,并且volatile会导致JIT发出围栏。dotnet/runtime#36697删除了每个排队到线程池的工作项的几个volatile访问,使ARM上的线程池更快。dotnet/runtime#34225将ConcurrentDictionary中的volatile访问从一个循环中抛出,这反过来提高了ARM上ConcurrentDictionary的一些成员的吞吐量高达30%。而dotnet/runtime#36976则完全从另一个ConcurrentDictionary字段中删除了volatile。
多年来,c#已经获得了大量有价值的特性。这些特性中的许多都是为了让开发人员能够更简洁地编写代码,而语言/编译器负责所有样板文件,比如c# 9中的记录。然而,有一些特性更注重性能而不是生产力,这些特性对核心库来说是一个巨大的恩惠,它们可以经常使用它们来提高每个人的程序的效率。来自@benaadams的dotnet/runtime#27195就是一个很好的例子。PR改进了Dictionary<TKey, TValue>,利用了c# 7中引入的ref返回和ref局部变量。>的实现是由字典中的数组条目支持的,字典有一个核心例程用于在其条目数组中查找键的索引;然后在多个函数中使用该例程,如indexer、TryGetValue、ContainsKey等。但是,这种共享是有代价的:通过返回索引并将其留给调用者根据需要从槽中获取数据,调用者将需要重新索引到数组中,从而导致第二次边界检查。有了ref返回,共享例程就可以把一个ref递回给槽,而不是原始索引,这样调用者就可以避免第二次边界检查,同时也避免复制整个条目。PR还包括对生成的程序集进行一些低级调优、重新组织字段和用于更新这些字段的操作,以便JIT能够更好地调优生成的程序集。
字典<TKey,TValue>的性能进一步提高了几个PRs。像许多哈希表一样,Dictionary<TKey,TValue>被划分为“bucket”,每个bucket本质上是一个条目链表(存储在数组中,而不是每个项都有单独的节点对象)。对于给定的键,一个哈希函数(TKey ' s GetHashCode或提供的IComparer ' s GetHashCode)用于计算提供的键的哈希码,然后该哈希码确定地映射到一个bucket;找到bucket之后,实现将遍历该bucket中的条目链,查找目标键。该实现试图保持每个bucket中的条目数较小,并在必要时进行增长和重新平衡以维护该条件。因此,查找的很大一部分开销是计算hashcode到bucket的映射。为了帮助在bucket之间保持良好的分布,特别是当提供的TKey或比较器使用不太理想的哈希代码生成器时,字典使用质数的bucket,而bucket映射由hashcode % numBuckets完成。但是在这里重要的速度,%操作符采用的除法是相对昂贵的。基于Daniel Lemire的工作,dotnet/coreclr#27299(来自@benaadams)和dotnet/runtime#406改变了64位进程中%的使用,而不是使用一对乘法和移位来实现相同的结果,但更快。
Sum .NET FW 4.8 77.45 us 1.00
Sum .NET Core 3.1 67.35 us 0.87
Sum .NET 5.0 44.10 us 0.57