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


这本身就是这次迁移的一个很好的好处,因为我们在.NET5中通过dotnet/runtime#37630 添加了System.Half,一个新的原始16位浮点,并且在托管代码中,这个排序实现的优化几乎立即应用到它,而以前的本地实现需要大量的额外工作,因为没有c++标准类型的一半。但是,这里还有一个更有影响的性能优势,这让我们回到我开始讨论的地方:GC。


GC的一个有趣指标是“pause time”,这实际上意味着GC必须暂停运行时多长时间才能执行其工作。更长的暂停时间对延迟有直接的影响,而延迟是所有工作负载方式的关键指标。正如前面提到的,GC可能需要暂停线程为了得到一个一致的世界观,并确保它能安全地移动对象,但是如果一个线程正在执行C/c++代码在运行时,GC可能需要等到调用完成之前暂停的线程。因此,我们在托管代码而不是本机代码中做的工作越多,GC暂停时间就越好。我们可以使用相同的数组。排序的例子,看看这个。考虑一下这个程序:

using System; using System.Diagnostics; using System.Threading; class Program { public static void Main() { new Thread(() => { var a = new int[20]; while (true) Array.Sort(a); }) { IsBackground = true }.Start(); var sw = new Stopwatch(); while (true) { sw.Restart(); for (int i = 0; i < 10; i++) { GC.Collect(); Thread.Sleep(15); } Console.WriteLine(sw.Elapsed.TotalSeconds); } } }


这是让一个线程在一个紧密循环中不断地对一个小数组排序,而在主线程上,它执行10次GCs,每次GCs之间大约有15毫秒。我们预计这个循环会花费150毫秒多一点的时间。但当我在.NET Core 3.1上运行时,我得到的秒数是这样的

6.6419048 5.5663149 5.7430339 6.032052 7.8892468


在这里,GC很难中断执行排序的线程,导致GC暂停时间远远高于预期。幸运的是,当我在 .NET5 上运行这个时,我得到了这样的数字:

0.159311 0.159453 0.1594669 0.1593328 0.1586566


这正是我们预测的结果。通过移动数组。将实现排序到托管代码中,这样运行时就可以在需要时更容易地挂起实现,我们使GC能够更好地完成其工作。


当然,这不仅限于Array.Sort。 一堆PR进行了这样的移植,例如dotnet/runtime#32722将stdelemref和ldelemaref JIT helper 移动到C#,dotnet/runtime#32353 将unbox helpers的一部分移动到C#(并使用适当的GC轮询位置来检测其余部分) GC在其余位置适当地暂停),dotnet/coreclr#27603 / dotnet/coreclr#27634 / dotnet/coreclr#27123 / dotnet/coreclr#27776 移动更多的数组实现,如Array.Clear和Array.Copy到C#, dotnet/coreclr#27216 将更多Buffer移至C#,而dotnet/coreclr#27792将Enum.CompareTo移至C#。 这些更改中的一些然后启用了后续增益,例如 dotnet/runtime#32342和dotnet/runtime#35733,它们利用Buffer.Memmove的改进来在各种字符串和数组方法中获得额外的收益。


关于这组更改的最后一个想法是,需要注意的另一件有趣的事情是,在一个版本中所做的微优化是如何基于后来被证明无效的假设的,并且当使用这种微优化时,需要准备并愿意适应。在我的.NET Core 3.0博客中,我提到了像dotnet/coreclr#21756这样的“peanut butter”式的改变,它改变了很多使用数组的调用站点。复制(源,目标,长度),而不是使用数组。复制(source, sourceOffset, destination, destinationOffset, length),因为前者获取源数组和目标数组的下限的开销是可测量的。但是通过前面提到的将数组处理代码移动到c#的一系列更改,更简单的重载的开销消失了,使其成为这些操作更简单、更快的选择。这样,.NET5 PRs dotnet/coreclr#27641和dotnet/corefx#42343切换了所有这些呼叫站点,更多地回到使用更简单的过载。dotnet/runtime#36304是另一个取消之前优化的例子,因为更改使它们过时或实际上有害。你总是能够传递一个字符到字符串。分裂,如version.Split (' . ')。然而,问题是,这个绑定到Split的唯一重载是Split(params char[] separator),这意味着每次这样的调用都会导致c#编译器生成一个char[]分配。为了解决这个问题,以前的版本添加了缓存,提前分配数组并将它们存储到静态中,然后可以被分割调用使用,以避免每个调用都使用char[]。既然.NET中有一个Split(char separator, StringSplitOptions options = StringSplitOptions. none)重载,我们就不再需要数组了。
作为最后一个示例,我展示了将代码移出运行时并转移到托管代码中如何帮助GC暂停,但是当然还有其他方式可以使运行时中剩余的代码对此有所帮助。dotnet/runtime#36179通过确保运行时处于代码下(例如获取“Watson”存储桶参数(基本上是一组用于唯一标识此特定异常和调用堆栈以用于报告目的的数据)),从而减少了由于异常处理而导致的GC暂停。 。暂停。

JIT

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

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