.NET 7的性能改进有哪些具体细节?
摘要:原文 | Stephen Toub 翻译 | 郑子铭 Mono 到目前为止,我一直提到 "JIT"、"GC "和 &qu
原文 | Stephen Toub
翻译 | 郑子铭
Mono
到目前为止,我一直提到 "JIT"、"GC "和 "运行时",但实际上在.NET中存在多个运行时。我一直在谈论 "coreclr",它是推荐在Linux、macOS和Windows上使用的运行时。然而,还有 "mono",它为Blazor wasm应用程序、Android应用程序和iOS应用程序提供动力。它在.NET 7中也有明显的改进。
就像coreclr(它可以JIT编译,AOT编译部分JIT回退,以及完全Native AOT编译),mono有多种实际执行代码的方式。其中一种方式是解释器,它使mono能够在不允许JIT的环境中执行.NET代码,而不需要提前编译或招致它可能带来的任何限制。有趣的是,解释器本身几乎就是一个成熟的编译器,它解析IL,为其生成自己的中间表示法 (intermediate representation)(IR),并在IR上进行一次或多次优化;只是在流水线的末端,当编译器通常会发出代码时,解释器却将这些数据保存下来,以便在运行时进行解释。因此,解释器有一个与我们讨论的coreclr的JIT非常相似的难题:优化的时间与快速启动的愿望。在.NET 7中,解释器采用了类似的解决方案:分层编译。 dotnet/runtime#68823增加了解释器的能力,最初编译时对IR进行最小的优化,然后一旦达到一定的调用次数阈值,就花时间对IR进行尽可能多的优化,用于该方法的所有未来调用。这产生了与coreclr相同的好处:改善了启动时间,同时也有高效的持续吞吐量。当这一点合并后,我们看到Blazor wasm应用程序的启动时间改善了10-20%。下面是我们的基准测试系统中正在跟踪的一个应用的例子。
不过,解释器并不只是用于整个应用程序。就像coreclr可以在R2R图像不包含方法的代码时使用JIT一样,mono可以在一个方法没有AOT代码时使用解释器。在mono上发生的这种情况是泛型委托的调用,在这种情况下,泛型委托的调用会触发回落到解释器;对于.NET 7,这种差距已经通过dotnet/runtime#70653解决。然而,一个更有影响的案例是dotnet/runtime#64867。以前,任何带有catch或filter异常处理条款的方法都不能被AOT编译,而会退回到被解释的状态。有了这个PR,方法现在可以被AOT编译,而且只有当异常真正发生时,它才会退回到使用解释器,在该方法调用的剩余执行过程中切换到解释器。由于许多方法都包含这样的条款,这可以使吞吐量和CPU消耗有很大的不同。同样地,dotnet/runtime#63065使带有finally异常处理条款的方法能够被AOT编译;只有finally块被解释,而不是整个方法被解释。
除了这样的后端改进,另一类改进来自coreclr和mono之间的进一步统一。几年前,coreclr和mono有自己的整个库堆栈,建立在它们之上。随着时间的推移,随着.NET的开源,mono的部分栈被共享组件一点一点地取代。时至今日,无论采用哪种运行时,System.Private.CoreLib以上的所有核心.NET库都是一样的。事实上,CoreLib本身的源代码几乎完全是共享的,大约95%的源文件被编译到为每个运行时构建的CoreLib中,只有百分之几的源文件是专门为每个运行时准备的(这些声明意味着本篇文章其余部分讨论的绝大多数性能改进无论在mono和coreclr上运行都同样适用)。即使如此,现在的每一个版本我们都在努力减少剩下的百分之几,这不仅是出于可维护性的考虑,而且还因为从性能的角度来看,用于coreclr的CoreLib的源代码通常会得到更多的关注。例如,dotnet/runtime#71325将mono的数组和跨度排序通用排序工具类转移到coreclr使用的更有效的实现。
然而,最大的改进类别之一是矢量化。这分为两部分。首先,由于dotnet/runtime#64961、dotnet/runtime#65086、dotnet/runtime#65128、dotnet/runtime#66317、dotnet/runtime#66391、dotnet/runtime#66409、dotnet/runtime#66512、 dotnet/runtime#66586、 dotnet/runtime#66589、 dotnet/runtime#66597、 dotnet/runtime#66476和 dotnet/runtime#67125;等PR,Vector和Vector128现在在x64和Arm64上都被完全加速了。
