.NET5 也是即时(JIT)编译器的一个令人兴奋的版本,该版本中包含了各种各样的改进。与任何编译器一样,对JIT的改进可以产生广泛的影响。通常,单独的更改对单独的代码段的影响很小,但是这样的更改会被它们应用的地方的数量放大。
可以向JIT添加的优化的数量几乎是无限的,如果给JIT无限的时间来运行这种优化,JIT就可以为任何给定的场景创建最优代码。但是JIT的时间并不是无限的。JIT的“即时”特性意味着它在应用程序运行时执行编译:当调用尚未编译的方法时,JIT需要按需为其提供汇编代码。这意味着在编译完成之前线程不能向前推进,这反过来意味着JIT需要在应用什么优化以及如何选择使用有限的时间预算方面有策略。各种技术用于给JIT更多的时间,比如使用“提前”(AOT)编译应用程序的一些部分做尽可能多的编译工作前尽可能执行应用程序(例如,AOT编译核心库都使用一个叫“ReadyToRun”的技术,你可能会听到称为“R2R”甚至“crossgen”,是产生这些图像的工具),或使用“tiered compilation”,它允许JIT在最初编译一个应用了从少到少优化的方法,因此速度非常快,只有在它被认为有价值的时候(即该方法被重复使用的时候),才会花更多的时间使用更多优化来重新编译它。然而,更普遍的情况是,参与JIT的开发人员只是选择使用分配的时间预算进行优化,根据开发人员编写的代码和他们使用的代码模式,这些优化被证明是有价值的。这意味着,随着.NET的发展并获得新的功能、新的语言特性和新的库特性,JIT也会随着适合于编写的较新的代码风格的优化而发展。
一个很好的例子是@benaadams的dotnet/runtime#32538。 Span 一直渗透到.NET堆栈的所有层,因为从事运行时,核心库,ASP.NET Core的开发人员以及其他人在编写安全有效的代码(也统一了字符串处理)时认识到了它的强大功能 ,托管数组,本机分配的内存和其他形式的数据。 类似地,值类型(结构)被越来越普遍地用作通过堆栈分配避免对象分配开销的一种方式。 但是,对此类类型的严重依赖也给运行时带来了更多麻烦。 coreclr运行时使用,这意味着GC能够100%准确地跟踪哪些值引用托管对象,哪些值不引用托管对象; 这样做有好处,但也有代价(相反,mono运行时使用“conservative”垃圾收集器,这具有一些性能上的好处,但也意味着它可以解释堆栈上的任意值,而该值恰好与 被管理对象的地址作为对该对象的实时引用)。 这样的代价之一是,JIT需要通过确保在GC注意之前将任何可以解释为对象引用的局部都清零来帮助GC。 否则,GC可能最终会在尚未设置的本地中看到一个垃圾值,并假定它引用的是有效对象,这时可能会发生“bad things”。 参考当地人越多,需要进行的清理越多。 如果您只清理一些当地人,那可能不会引起注意。 但是随着数量的增加,清除这些本地对象所花费的时间可能加起来,尤其是在非常热的代码路径中使用的一种小方法中。 这种情况在跨度和结构中变得更加普遍,在这种情况下,编码模式通常会导致需要为零的更多引用(Span 包含引用)。 前面提到的PR通过更新JIT生成的序号块的代码来解决此问题,这些序号块使用xmm寄存器而不是rep stosd指令来执行该清零操作。 有效地,它对归零进行矢量化处理。 您可以通过以下基准测试看到此影响:
【翻译】.NET 5中的性能改进 (4)
内容版权声明:除非注明,否则皆为本站原创文章。