【翻译】.NET 5中的性能改进 (2)


对于所有对.NET和性能感兴趣的人来说,垃圾收集通常是他们最关心的。在减少分配上花费了大量的精力,不是因为分配行为本身特别昂贵,而是因为通过垃圾收集器(GC)清理这些分配之后的后续成本。然而,无论减少分配需要做多少工作,绝大多数工作负载都会导致这种情况发生,因此,重要的是要不断提高GC能够完成的任务和速度。


这个版本在改进GC方面做了很多工作。例如, dotnet/coreclr#25986 为GC的“mark”阶段实现了一种形式的工作窃取。.NET GC是一个“tracing”收集器,这意味着(在非常高的级别上)当它运行时,它从一组“roots”(已知的固有可访问的位置,比如静态字段)开始,从一个对象遍历到另一个对象,将每个对象“mark”为可访问;在所有这些遍历之后,任何没有标记的对象都是不可访问的,可以收集。此标记代表了执行集合所花费的大部分时间,并且此PR通过更好地平衡集合中涉及的每个线程执行的工作来改进标记性能。当使用“Server GC”运行时,每个核都有一个线程参与收集,当线程完成分配给它们的标记工作时,它们现在能够从其他线程“steal” 未完成的工作,以帮助更快地完成整个收集。


另一个例子是,dotnet/runtime#35896 “ephemeral”段的解压进行了优化(gen0和gen1被称为 “ephemeral”,因为它们是预期只持续很短时间的对象)。在段的最后一个活动对象之后,将内存页返回给操作系统。那么GC的问题就变成了,这种解解应该在什么时候发生,以及在任何时候应该解解多少,因为在不久的将来,它可能需要为额外的分配分配额外的页面。


或者以dotnet/runtime#32795,为例,它通过减少在GC静态扫描中涉及的锁争用,提高了在具有较高核心计数的机器上的GC可伸缩性。或者dotnet/runtime#37894,它避免了代价高昂的内存重置(本质上是告诉操作系统相关的内存不再感兴趣),除非GC看到它处于低内存的情况。或者dotnet/runtime#37159,它(虽然还没有合并,预计将用于.NET5 )构建在@damageboy的工作之上,用于向量化GC中使用的排序。或者 dotnet/coreclr#27729,它减少了GC挂起线程所花费的时间,这对于它获得一个稳定的视图,从而准确地确定正在使用的线程是必要的。


这只是改进GC本身所做的部分更改,但最后一点给我带来了一个特别吸引我的话题,因为它涉及到近年来我们在.NET中所做的许多工作。在这个版本中,我们继续,甚至加快了从C/C++移植coreclr运行时中的本地实现,以取代System.Private.Corelib中的普通c#托管代码。此举有大量的好处,包括让我们更容易共享一个实现跨多个运行时(如coreclr和mono),甚至对我们来说更容易进化API表面积,如通过重用相同的逻辑来处理数组和跨越。但让一些人吃惊的是,这些好处还包括多方面的性能。其中一种方法回溯到使用托管运行时的最初动机:安全性。默认情况下,用c#编写的代码是“safe”,因为运行时确保所有内存访问都检查了边界,只有通过代码中可见的显式操作(例如使用unsafe关键字,Marshal类,unsafe类等),开发者才能删除这种验证。结果,作为一个开源项目的维护人员,我们的工作的航运安全系统在很大程度上使当贡献托管代码的形式:虽然这样的代码可以当然包含错误,可能会通过代码审查和自动化测试,我们可以晚上睡得更好知道这些bug引入安全问题的几率大大降低。这反过来意味着我们更有可能接受托管代码的改进,并且速度更快,贡献者提供的更快,我们帮助验证的更快。我们还发现,当使用c#而不是C时,有更多的贡献者对探索性能改进感兴趣,而且更多的人以更快的速度进行实验,从而获得更好的性能。


然而,我们从移植中看到了更直接的性能改进。托管代码调用运行时所需的开销相对较小,但是如果调用频率很高,那么开销就会增加。考虑dotnet/coreclr#27700,它将原始类型数组排序的实现从coreclr的本地代码移到了Corelib的c#中。除了这些代码之外,它还为新的公共api提供了对跨度进行排序的支持,它还降低了对较小数组进行排序的成本,因为排序的成本主要来自于从托管代码的转换。我们可以在一个小的基准测试中看到这一点,它只是使用数组。对包含10个元素的int[], double[]和string[]数组进行排序:

public class DoubleSorting : Sorting<double> { protected override double GetNext() => _random.Next(); } public class Int32Sorting : Sorting<int> { protected override int GetNext() => _random.Next(); } public class StringSorting : Sorting<string> { protected override string GetNext() { var dest = new char[_random.Next(1, 5)]; for (int i = 0; i < dest.Length; i++) dest[i] = (char)('a' + _random.Next(26)); return new string(dest); } } public abstract class Sorting<T> { protected Random _random; private T[] _orig, _array; [Params(10)] public int Size { get; set; } protected abstract T GetNext(); [GlobalSetup] public void Setup() { _random = new Random(42); _orig = Enumerable.Range(0, Size).Select(_ => GetNext()).ToArray(); _array = (T[])_orig.Clone(); Array.Sort(_array); } [Benchmark] public void Random() { _orig.AsSpan().CopyTo(_array); Array.Sort(_array); } } Type Runtime Mean Ratio
DoubleSorting   .NET FW 4.8   88.88 ns   1.00  
DoubleSorting   .NET Core 3.1   73.29 ns   0.83  
DoubleSorting   .NET 5.0   35.83 ns   0.40  
       
Int32Sorting   .NET FW 4.8   66.34 ns   1.00  
Int32Sorting   .NET Core 3.1   48.47 ns   0.73  
Int32Sorting   .NET 5.0   31.07 ns   0.47  
       
StringSorting   .NET FW 4.8   2,193.86 ns   1.00  
StringSorting   .NET Core 3.1   1,713.11 ns   0.78  
StringSorting   .NET 5.0   1,400.96 ns   0.64  

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsssyf.html