我对几个应用进行严格的启动性能评估,对比了在 .NET Framework 和 dotnet 6 下的应用启动性能,非常符合预期的可以看到,在用户的设备上,经过了 NGen 之后的 .NET Framework 可以提供非常优越的启动性能,再加上 .NET Framework 本身就是属于系统组件的部分,很少存在冷启动的时候,大部分的 DLL 都在系统里预热。启动性能方面,依然是 .NET Framework 比 dotnet 6 快非常多。而在破坏了 .NET Framework 的运行时框架层的 NGen 之后,可以发现 .NET Framework 的启动性能就比不过 dotnet 6 的启动性能。为了在 dotnet 6 下追平和 .NET Framework 的启动性能差异,引入与 NGen 的同等级的 ReadyToRun 用来提升整体的性能。本文将告诉大家如何在 dotnet 6 的应用里面,使用 Crossgen2 工具,给 DLL 生成 AOT 数据,提升应用启动性能

我预计本文是具有时效的,各个概念都在变更,本文是在 2022.05 编写的。如果你阅读本文的时间距离本文编写时间过长,那请小心本文过期的知识误导

开始之前,还请理清一下概念

在 dotnet 里面,这些概念都在变来变去,还没有完全定下来。在聊 dotnet 里面的 AOT 之前,是必须先来做一个辟谣的。第一个谣言是 AOT 意味着性能更高? 其实不然,采用 AOT 能减少应用启动过程中,从 IL 转换为本机代码的损耗,但通过分层编译(TieredCompilation)技术,这部分的差异不会特别特别大,再加上 dotnet 6 引入 的 QuickJit 技术,还能进一步缩小差距。但即使这么说,启动性能方面,采用 AOT 还是很有优势的,因为启动过程是性能敏感的,再加上大型项目在启动过程中将需要执行大量的代码逻辑,即使 JIT 再快和加上动态 PGO 的辅助下,依然由于需要工作的量太多而在性能上不如采用 AOT 的方式。由于 AOT 是生产静态逻辑,只取平台最小集,而无法和 JIT 一样,根据所运行设备进行动态优化,这就是为什么运行过程中的性能,在 JIT 进入 Tier 2 优化之后的性能要远远超过 AOT 的方式。换句话说,全程都使用 AOT 而不加入任何 JIT 只是提升启动性能,但是降低了运行过程的性能

那如果我启动性能也要,运行过程的性能也要呢?这个就是 ReadyToRun 技术的概念了,在 DLL 的进入调用时,先采用 AOT 技术,将部分逻辑预先跑了 JIT 且将跑了之后的二进制逻辑也记录到 DLL 里面。如此可以实现在首次调用方法时,减少 JIT 的戏份,尽可能使用之前 AOT 的内容,从而提升应用启动性能。而在应用跑起来之后,依然跑的是 JIT 的优化,如此即可兼顾启动性能和运行过程的性能

如何实现 ReadyToRun 这个概念?就需要用到几项技术和工具,其中 Crossgen2 就是进行 ReadyToRun 的工具。通过 Crossgen2 工具,可以对 DLL 进行静态 AOT 编入 DLL 内

但是如此做法也不是没有缺点的,那就是额外编入 DLL 的 AOT 的内容,将会增大 DLL 的体积。而 DLL 体积的增大将会降低启动过程中读取文件的性能,再加上 AOT 和 JIT 过程的切换也是需要判断逻辑,加上了这部分损耗之后,再对比一下 QuickJit 技术,实际上采用 Crossgen2 进行 ReadyToRun 不是对所有的 DLL 都能提升启动性能

为了解决以上问题,在 dotnet 里再引入了 PGO 的概念。启动过程里面调用的方法是有限的,如果可以了解到应用启动过程将会调用哪些方法,只是将这部分方法进行 AOT 那么对 DLL 体积的影响将会小非常多。这就是 PGO 需要解决的问题,通过引入 PGO 这个概念,在应用运行过程里面,了解应用启动过程将会碰到哪些 IL 逻辑,将这部分逻辑记录下来,用于指导 ReadyToRun 过程进行 AOT 哪些方法。从而让 AOT 过程不需要针对所有的 IL 逻辑,而是仅对应用启动过程需要用到的才进行 AOT 过程。如此即可更大的提升应用的启动性能。不过 PGO 可以做的事情可不只是 ReadyToRun 的指导,还可以作为 JIT 过程中,让 JIT 了解可以预先在后台线程里面跑哪些 IL 转换从而达到更高的启动性能。必须说明的是,我询问了几位大佬了解到,当前的 PGO 还是一个玩具,虽然性能评测上可以达到很好的效果,然而还没有具备发布环境使用的能力

对于 AOT 不可反编译的辟谣。如上文可以看到 ReadyToRun 技术上,依然是保留 IL 逻辑,只是在 DLL 里面再加入 AOT 生成的二进制数据,从而减少启动过程的 JIT 的损耗。也就是说如果采用 ReadyToRun 的技术,可以让应用有更快(不一定是更快)的启动性能,同时也拥有原本的运行过程的性能。但是否可以做到不可反编译,自然是做不到的,原本的 IL 代码依然还在,也就是说采用 ReadyToRun 技术,没有任何额外的保护能力。那第二个问题,如果采用纯 AOT 技术,能否达到代码保护能力?嗯,能加一点点。如果配合上混淆的话,感觉上是差不多了。如果要说防破解能力的话,两个的打分,一个是 60 分,一个是 70 分,满分是 100 分。真要别人看不懂,代码写垃圾些就好了,我全力发挥的时候,保证连自己都看不懂

回到主题,如何在 dotnet 里面通过 Crossgen2 工具进行 ReadyToRun 提升应用性能? 千万别被官方骗了,如果只是在 csproj 上或者是在发布的时候加上 ReadyToRun 的命令参数,恭喜你,是真的用了 Corssgen2 工具。但优化呢?只是优化了入口程序集而已

真的想要有比较大的优化,是需要将除了入口程序集之外的其他程序集也通过 Crossgen2 工具进行 ReadyToRun 才可以有比较大的提升的。例如我的一个大型应用,在启动过程里面将 WPF 框架里面大概十分之一的模块都碰了一次,使用 JitInfo.GetCompiledMethodCount 了解到,在第一个窗口 Show 出来之前就有 5 万个方法调用。这个应用的入口程序集占比太小了,如果使用官方的方法,只是对入口程序集进行 ReadyToRun 那么性能上还真被 .NET Framework 完虐

为了让 dotnet 6 应用的启动性能能媲美 .NET Framework 应用的启动性能,可以采用 ReadyToRun 对标 .NET Framework 的 NGen 技术。以下将告诉大家如何使用 Crossgen2 工具对 DLL 进行 ReadyToRun 提升启动性能

默认的 Crossgen2 工具是采用 NuGet 分发的 DotnetPlatform 类型的 NuGet 包,里面包含了独立发布的 Crossgen2 工具。换句话说,可以在 %localappdata%\..\..\.nuget\packages\microsoft.netcore.app.crossgen2.win-x64 找到此工具。如果没有找到的话,那试试用一句 dotnet publish -c Release -r win-x64 -p:PublishReadyToRun=true 命令让 dotnet 为了构建 ReadyToRun 而帮你将 Crossgen2 下载

以上的 Crossgen2 工具放在 microsoft.netcore.app.crossgen2.win-x64 文件夹里面,这里的 win-x64 指的不是 Crossgen2 工具的能力,不是说这个文件夹的工具只能构建出 win-x64 的。而是说这个工具本身是 win-x64 的。这个工具是能构建出其他的平台的 AOT 的。换句话说是在 Windows 的 32 位系统里面,将会拉的工具是 microsoft.netcore.app.crossgen2.win-x86 的包

进入版本号文件夹,再进入 Tools 文件夹即可找到 Crossgen2.exe 可执行文件,这就是工具本文。例如在我的设备上的工具路径是

C:\Users\lindexi\.nuget\packages\microsoft.netcore.app.crossgen2.win-x64\6.0.5\tools\Crossgen2.exe

接下来将告诉大家如何使用这个工具

这个工具的使用需要传入的参数推荐是一个 rsp 文件,大概的命令行调用如下

C:\Users\lindexi\.nuget\packages\microsoft.netcore.app.crossgen2.win-x64\6.0.5\tools\Crossgen2.exe "@C:\lindexi\Fxx\F1.rsp"

具体的参数都放在 rsp 文件里面,大概内容如下

--targetos:windows
--targetarch:x86
--pdb
-O
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-console-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-console-l1-2-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-datetime-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-debug-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-errorhandling-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-fibers-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-file-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-file-l1-2-0.dll"
--out:"C:\Users\linde\AppData\Local\Temp\Crossgen2\Crossgen2\KokicakawheeyeeWhemhedawfelawnemhel.dll"
C:\lindexi\Code\empty\KokicakawheeyeeWhemhedawfelawnemhel\KokicakawheeyeeWhemhedawfelawnemhel\bin\release\net6.0-windows\win-x86\publish\KokicakawheeyeeWhemhedawfelawnemhel.dll

大概由以下几个部分组成。每一行都是一个独立的参数,分别内容如下

  • --targetos:windows: 准备执行的系统平台。进行 ReadyToRun 将生成 AOT 代码,这是平台强相关的,必须说明是哪个平台
  • --targetarch:x86: 准备生成的对应平台,是 x86 还是 x64 等
  • --pdb: 这是可选的,表示要生成 PDB 符号文件。如不加上这一句将不生成 PDB 文件。生成的 PDB 文件是 ni.pdb 文件,配合原本的 DLL 的 PDB 文件即可方便进行调试
  • -O: 这是可选的,表示需要进行优化。相当于 Release 版本。推荐默认都加上,否则将几乎没有优化效果,或者说只有反向优化效果
  • -r:"xxx.dll": 这里将会重复很多行,一行一个程序集文件的本地路径。让工具了解到有哪些引用可以去找到。工具在准备 AOT 过程,需要找到所引用的程序集。这些参数就是告诉工具对应的程序集放在哪。可以多加入很多程序集,因为只是给工具使用的参考引用,工具会根据自己的需求,去找到对应的程序集文件。如果工具发现传入的有多余的,那将会自动忽略多余的。推荐将整个 dotnet runtime 都加入,但是要注意加入的版本必须是和发布的版本是一致的,否则启动过程如果炸了,那就凉凉。如果应用是独立发布的,那就列出应用独立发布文件夹里面的所有 DLL 文件,不需要加上额外的运行时文件夹
  • --out:"xx.dll": 处理之后的输出文件路径
  • xxxxx.dll 输入程序集的路径

构建出 rsp 文件,作为参数,调用 Crossgen2 工具,即可完成对程序集的 ReadyToRun 过程。多个程序集就多次重复以上过程即可

必须画重点的是,调用 Crossgen2 工具进行 ReadyToRun 是不一定能提升启动性能的,这是一个需要测量的过程。每个 DLL 在调用了 Crossgen2 工具进行 ReadyToRun 是会修改文件体积的,整个变更也是会影响启动性能的。推荐在优化应用启动性能,进行足够的测量,方法如下

使用 Crossgen2 工具对每个 DLL 来一次,包括框架层的 DLL 也来一次。然后逐个 DLL 替换,测量应用启动性能。如果发现某些 DLL 进行了 ReadyToRun 反而降低启动性能,或者某些 DLL 加大的文件体积对比启动性能的优化来说不划算,那就不对这些 DLL 进行优化

以下是测试的对 dotnet runtime 底层和 WPF 框架的 DLL 进行 ReadyToRun 优化之后,对 walterlv 大佬的某个应用的启动性能的影响,值得一提的是对于不同的应用,测试的数据将会存在很大的出入,核心原因在于不同的应用启动过程将访问的模块有所不同

这个数据是没有多少参考价值的,因为对于不同的应用来说,以上的结果将会有变化。如果你想要采用 ReadyToRun 技术提升应用启动性能,还请必须测量每个 DLL 在经过 ReadyToRun 对启动性能的影响。如果你的时间充裕的话,还可以测量对多个 DLL 优化的组合对启动性能的影响

我所在团队的某个大型应用,在经过了 ReadyToRun 技术的优化,启动性能提升百分之三十

但也必须说明的是,不是所有的应用使用 ReadyToRun 都能有优化启动性能,例如我的一个小应用,只要采用了 ReadyToRun 技术,启动性能基本上都是降低了。总的来说,采用 ReadyToRun 技术是需要进行性能测量的

参考文档

WPF dotnet 使用本机映像 native 优化 dotnet framework 二进制文件

WPF 通过 ReadyToRun 提升性能

Conversation about crossgen2 - .NET Blog

runtime/crossgen2-compilation-structure-enhancements.md at main · dotnet/runtime

runtime/Program.cs at main · dotnet/runtime

编译配置设置 - .NET Microsoft Docs

ReadyToRun deployment overview - .NET Microsoft Docs

ReadyToRun deployment overview - .NET Microsoft Docs

利用 PGO 提升 .NET 程序性能 - hez2010 - 博客园

JitInfo.GetCompiledMethodCount(Boolean) Method (System.Runtime) Microsoft Docs

dotnet 使用 Crossgen2 对 DLL 进行 ReadyToRun 提升启动性能的更多相关文章

  1. dotnet 启动 JIT 多核心编译提升启动性能

    用2分钟提升十分之一的启动性能,通过在桌面程序启动 JIT 多核心编译提升启动性能 在 dotnet 可以通过让 JIT 进行多核心编译提升软件的启动性能,在默认托管的 ASP.NET 程序是开启的, ...

  2. 2019-8-31-dotnet-启动-JIT-多核心编译提升启动性能

    title author date CreateTime categories dotnet 启动 JIT 多核心编译提升启动性能 lindexi 2019-08-31 16:55:58 +0800 ...

  3. [.net 面向对象程序设计进阶] (15) 缓存(Cache)(二) 利用缓存提升程序性能

    [.net 面向对象程序设计进阶] (15) 缓存(Cache)(二) 利用缓存提升程序性能 本节导读: 上节说了缓存是以空间来换取时间的技术,介绍了客户端缓存和两种常用服务器缓布,本节主要介绍一种. ...

  4. 提升PHP性能的21种方法

    提升PHP性能的21种方法. 1.用单引号来包含字符串要比双引号来包含字符串更快一些.因为PHP会在双引号包围的字符串中搜寻变量,单引号则不会.2.如果能将类的方法定义成static,就尽量定义成st ...

  5. HHVM 是如何提升 PHP 性能的?

    背景 HHVM 是 Facebook 开发的高性能 PHP 虚拟机,宣称比官方的快9倍,我很好奇,于是抽空简单了解了一下,并整理出这篇文章,希望能回答清楚两方面的问题: HHVM 到底靠谱么?是否可以 ...

  6. 使用异步HTTP提升客户端性能(HttpAsyncClient)

    使用异步HTTP提升客户端性能(HttpAsyncClient) 大家都知道,应用层的网络模型有同步.异步之分. 同步,意为着线程阻塞,只有等本次请求全部都完成了,才能进行下一次请求. 异步,好处是不 ...

  7. 极光开发者沙龙 之 移动应用性能优化实践 【一】旧酒新瓶——换个角度提升 App 性能与质量

    旧酒新瓶--换个角度提升 App 性能与质量 主讲人:高亮亮 ---   饿了么移动技术部高级iOS工程师,负责饿了么商家版iOS APP开发,对架构和系统底层有深入研究,擅长移动性能分析,troub ...

  8. React爬坑秘籍(一)——提升渲染性能

    React爬坑秘籍(一)--提升渲染性能 ##前言 来到腾讯实习后,有幸八月份开始了腾讯办公助手PC端的开发.因为办公助手主推的是移动端,所以导师也是大胆的让我们实习生来技术选型并开发,他来做code ...

  9. 十个技巧迅速提升JQuery性能

    本文提供即刻提升你的脚本性能的十个步骤.不用担心,这并不是什么高深的技巧.人人皆可运用!这些技巧包括: 使用最新版本 合并.最小化脚本 用for替代each 用ID替代class选择器 给选择器指定前 ...

随机推荐

  1. Java基础语法01——变量与运算符

    本文是对Java基础语法的第一部分的学习,包括注释:标识符的命名规则与规范:变量的数据类型分类以及转换:以及六种运算符(算术.赋值.比较.逻辑.三元和位运算符).

  2. @JsonFormat、@DateTimeFormat、@JsonSerialize注解的使用

    @JsonFormat 是jackson的注解,用于后台返回前台的时候将后台的date类型数据转为string类型格式化显示在前台,加在get方法或者date属性上面,因为 @JsonFormat 注 ...

  3. Java学习day14

    可变参数用作方法的形参,方法参数的个数就可变 格式:修饰符 返回值类型 方法名(数据类型...变量名){ } 方法内的形参只能有一个,这里的变量是一个数组 public static <T> ...

  4. MySql创建分区

    一.Mysql分区类型 1.RANGE 分区:基于属于一个给定连续区间的列值,把多行分配给分区. 2.HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列 ...

  5. POJ - 1321 A - 棋盘问题

    A - 棋盘问题 http://poj.org/problem?id=1321 思路:不能搞双重循环嵌套,要注意可以跳过某行 代码 #include <cstdio> #include & ...

  6. Python简单爬取Amazon图片-其他网站相应修改链接和正则

    简单爬取Amazon图片信息 这是一个简单的模板,如果需要爬取其他网站图片信息,更改URL和正则表达式即可 1 import requests 2 import re 3 import os 4 de ...

  7. VMWARE vcenter重置root密码

    1\重启VCSA 2\在GNU GRUBc的时候,按住e键,在后面加上一句命令 3.rw init=/bin/bash 4. 按CTRL-X或者按住F10,启动系统 5. 使用passwd命令修改ro ...

  8. C# 核心

    C# 核心 面向对象编程概念 面向过程编程是一种以过程为中心的编程思想,分析出解决问题所需要的步骤,然后有函数把步骤一步一步实现,使用的时候一个一个依次调用. 面向对象是一种对现实世界理解和抽象的编程 ...

  9. go-micro集成链路跟踪的方法和中间件原理

    前几天有个同学想了解下如何在go-micro中做链路跟踪,这几天正好看到wrapper这块,wrapper这个东西在某些框架中也称为中间件,里边有个opentracing的插件,正好用来做链路追踪.o ...

  10. web框架的本质、MVC框架MTV框架的介绍

    1.web框架的本质 所有的Web应用本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端,基于请求做出响应,客户都先请求,服务端做出对应的响应,按照http协议的请求协议发送请 ...