AOT和单文件发布对程序性能的影响
前言
这里先和大家介绍一下.NET一些发布的历史,以前的.NET框架原生并不支持最终编译结果的单文件发布(需要依赖第三方工具),我这里新建了一个简单的ASP.NET Core项目,发布以后的目录就会像下图这样,里面包含很多*.dll
文件和其它各类的文件。
在.NET Core 2.1时代,引入了单文件发布的功能,只需要在发布命令上,增加-p:PublishSingleFile=true
参数就可以使用,从这以后就无需发布的文件夹就再也没有那么多的文件,只有一个*.exe
文件和对应的配置文件和用于调试*.pdb
的文件,如下所示:
不过此时的.NET还是需要安装一个大小为50~130MB左右的.NET Runtime才能运行,这个其实不利于在客户端场景下程序的分发,大家应该能回忆起在安装一些软件之前,必须安装.NET Framework的场景。
在单文件发布推出的同时,也可以通过--self-contained true
的参数,将运行时也包含在发布文件内,这样的话就无需在目标机器上再安装.NET Runtime。不过由于它自带运行时,整个发布文件夹的大小就变得很大了,可以说比安装.NET Runtime还要大一些(足足82.4MB)。
程序本质上也就是文件,我们也可以通过压缩程序的方式,让它的大小变小,只需要加上-p:EnableCompressionInSingleFile=true
参数。就可以将80MB的程序压缩至44MB左右。
单文件发布体积大的原因就是包括了所有运行可能用到的依赖,不过有很多依赖是我们程序中用不到的,所以发布的时候可以加-p:PublishTrimmed=true
参数,发布的时候移除掉没有使用的依赖,这样体积就可以降低很多(从44MB到35MB)。
当然,移除没有使用的依赖和压缩是可以同时使用的,这样发布以后,体积就可以变得更小了,只需要20MB左右。
此时.NET运行还是需要自带运行时,在运行.NET程序的时候需要JIT来参与,这样的话在应用启动时需要一定的时间让JIT将MSIL编译到对应平台机器码,随后.NET推出了预览版的Native-AOT
,可以在编译时直接将代码编译成对应平台的机器码,以加快启动速度;另外由于不需要自带运行时,它整个的体积大小也变得很小。
用于调试的pdb
文件就会变得很大,不过真实发布的话也用不到这个文件,可以舍弃。AOT以后的大小也就20MB左右。不过AOT也不是银弹,由于没有了JIT,很多编译时优化就不能做了,Java的GraalVm发布的时候就有一张五边形图,充分的说明了JIT和AOT之间的取舍。
AOT拥有更快的启动速度、更低的内存占用和更小的程序体积;当然它的吞吐量和最大延时表现的就没那么好(另外也会失去很多动态的特性,降低一些编程效率)。
心中会有一个疑问,这样的发布方式会对程序的性能有影响嘛?都说AOT会让程序启动速度变快,那么会变快多少呢?
评测结果
我决定花点时间来研究一下,周末带着上面的问题我设计了一组测试,当然时间仓促有很多不严谨的地方,可以说就图一乐,望大家指出和海涵。一共设计了12个组,主要是对比单文件发布、AOT发布和普通发布的区别;另外我也加入了PGO、TC、OSR和OSA等JIT参数,来看看不同JIT参数的影响。
PGO:PGO 即 Profile Guided Optimization(配置引导优化),通过收集运行时信息来指导 JIT 如何优化代码,相比以前没有 PGO 时可以做更多以前难以完成的优化。可以参考hez大佬的博客,还有一些链接1、链接2、链接3.
TC:TC 即 Tiered Compilation(分层编译),是一种运行时优化代码的技术,每个C#函数都会由JIT编译成目标平台的机器码,为了让方法能快点运行,JIT一般会很粗犷(并不是最优,生成代码效率比较低)的编译,所以JIT就引入了TC,当某一个方法频繁被调用时,JIT就会为它编译一份更优的代码,这样下一次方法被调用时,它执行的会更有效率。想了解更多关于.NET分层编译可以戳这个链接。
OSR:OSR 即 On-Stack Replacement(栈上替换),OSR是一种在运行时替换正在运行的函数/方法的栈帧的技术。这个是为了分层编译引入的,因为有时候我们运行的方法是一个
while(ture)
这种死循环方法,分层编译找不到时机能把低优化的代码替换成高优化的代码,所以引入了栈上替换,在方法运行中就可以替换成更优的方法。链接1、链接2。
OSR:OSA 即 Object Stack Allocation (对象栈上分配),在.NET中的引用对象默认是分配在堆上的,回收时需要垃圾回收器介入,而且分配对象时必须初始化内存(全部初始化为0),如果对象的生命周期可控,那么可以将它分配在栈上。这样做的好处就是能降低GC压力(方法栈结束,对象自动释放了),提升性能(可以进行标量替换,访问更快)。链接1。
每个组的命名和参数如下所示。
项目 | 备注 |
---|---|
Normal | 正常发布,对照组 |
Normal-WksGC | 正常方式,使用WorkStationGC |
Normal_PGO | 正常发布,使用PGO |
Normal_PGO_OSR | 正常发布,使用OSR |
Normal_PGO_OSR_OSA | 正常发布,使用PGO+OSR+OSA |
SingleFilePublish | 普通单文件发布 |
SingleFilePublish-SelfContained | 包含运行时单文件发布 |
SingleFilePublish-SelfContained-Trim | 包含运行时单文件发布+剪裁程序集 |
SingleFilePublish-SelfContained-Compress | 包含运行时单文件发布+压缩程序集 |
SingleFilePublish-SelfContained-Trim-Compress | 包含运行时单文件发布+剪裁+压缩程序集 |
AOT-Size | AOT编译,使用Size模式 |
AOT-Speed | AOT编译,使用Speed模式 |
下方的小标题是评测项的方式和评测的结果,每个项我们都会跑5次,最后取平均值。
发布相关
在本节中,Normal那几项编译参数都是一样的,所以结果几乎没有差别,无需过多关注,忽略就好。
发布耗时
发布耗时这个参数,是记录了dotnet publish
的耗时,其中会清理/bin、/obj
等文件夹,避免缓存带来的影响。
可以看到单文件发布和AOT发布还是比较吃性能的,特别是AOT场景下简单的ASPNET Core项目的发布时间就到了接近30秒和一些Rust、C++项目编译速度有的一拼了,要是更大的项目估计会更长。不过正常发布还是很快的,不会一两秒内都能完成。
目录大小
目录大小是直接统计发布以后的目录所占用的硬盘空间,注意:Normal发布都计算了67.5MB的.NET Runtime占用的空间。
为什么AOT的目录大小会这么大呢?主要就是上文中提到的用于调试程序的pdb
文件变的很大,这是因为AOT以后程序本身缺失很多用于调试的数据,只能存放在pdb
文件中,不过这个对于使用没有什么影响,发布时也可以通过-p:DebugType=false
和-p:DebugSymbols=false
参数让它不生成pdb
文件。
程序大小
程序大小统计只发布文件中需要运行程序的大小,这个是和分发项目息息相关的,越小的程序体积,就越容易分发。注意:Normal发布都计算了67.5MB的.NET Runtime占用的空间。
如果目标平台已经预装了.NET Runtime,其实正常发布的效率是最高的,只有一百多KB的大小;次之就是单文件发布+自包含运行时+裁剪+压缩,大小只有20来MB,也比较利于分发。AOT的表现也同样亮眼。
程序运行相关
程序运行相关一共有三个指标,分别为启动耗时、应用启动耗时和内存占用,这里没有设置CPU相关的指标,是因为启动程序CPU基本都是0没有太大的参考意义。下方流程图展示了这几个指标的采集时间。
启动耗时
程序的启动耗时结果如下所示。
我们可以看到两个极值,最大的单文件+自包含运行时+压缩启动耗时到170ms,因为没有剪裁程序集,需要解压缩的依赖很大,所以启动耗时会比较长一点。最小的AOT-Speed模式只需要16.8ms就能启动程序,看来没有了JIT编译和程序集加载的过程,果然快很多。
应用启动耗时
应用启动耗时和程序启动耗时排列基本一致,像单文件+自包含运行时+压缩启动耗时需要0.5s+才能启动程序,而AOT模式只需要70ms,中间差了七八倍。不过正常发布启动速度也很快,只需要200ms不到的时间。
内存占用
内存占用各个方式差别不大,但是也提醒到了我们,如果想让内存占用小一些,那么可以使用WorkstationGC模式。引入动态PGO之类的JIT增强特性以后,相应的会多占用一些内存。
性能压测
机器配置:
CPU:I7 8750H 关闭超线程
RAM:48GB
Client:设置CPU亲和性,绑定3个核心
Server:设置CPU亲和性,绑定2个核心
由于笔者机器配置有限,没有做Client
和Server
的环境隔离,只做了简单的CPU绑核,所以的出来的数据仅供参考。
压测QPS
可以看到其实各个方式差别不是很大,都取得了4.7Wqps
以上的成绩,最大和最小在4%以内。由于这是IO密集型任务,JIT、PGO的优势没有体现出来,后面可以试试一些计算密集型的任务,或者直接看hez的博客,上文介绍PGO中有链接。
单次请求耗时
下图中在条形图内较大的是单次请求耗时(MAX)
,在条形图外的0.x
的数据是单次请求耗时(AVG)
。单位是ms
.
我们发现平均耗时基本在0.3ms
左右,AOT和单文件+自包含运行时+剪裁+压缩的表现很亮眼,只有370ms
左右。
压测内存占用
下图中深色代表内存占用(MAX)
而浅色代表内存占用(AVG)
,单位是MB
.
可以看到除了AOT以外的方式,内存占用是大差不差的,4.7Wqps
下只需要25MB
左右的内存其实很不错了,近似的数字可以理解为误差;另外开启了JIT特性以后,就需要占用更多的内存。AOT的话内存占用就比较多了,可能GC算法在AOT环境下的优化还不够。
压测CPU占用
下图中深色代表CPU占用(MAX)
而浅色代表CPU占用(AVG)
。单位为百分比
;1个CPU核心是100%
,如果占用5个CPU核心那么就是500%
。
基本上都没有啥区别,但是AOT方式占用率就小了很多,毕竟没有了JIT这个步骤。
总结
这个结论也就是图一乐,毕竟目前AOT还没有正式发布(已经合并主分支.NET7会正式发布),还有很多值得优化的地方。另外像OSR、OSA这些特性也还没有完全定下来,下面是一些和对照组比较的百分比数据,原始数据和测试代码见Github。后续.NET7正式发布了,再跑一下试试。
回答开始提到的问题,总得来说AOT对缩小软件大小,提升应用启动速度有着很大的作用,但是目前需要很长的发布时间和占用更多的内存。
另外PGO等一些JIT特性需要比正常情况下占用更多的内存,其性能的优势在这个IO密集的场景没有很好的表现出来。
最后在多说几句,我一直觉得C#是一个很好的语言,.NET是一个很好的平台。从2002年一路走来,今年是.NET的第20个年头,各种新特性相继加入,性能也已经站在了第一梯队,希望以后能有更多的发展吧。
PS:在前几天更新的Benchmarks Game数据里面,C# .NET已经是带JIT语言里面跑的最快的了,仅次于C、C++、Rust等编译型语言,详情可见链接1、链接2。
AOT和单文件发布对程序性能的影响的更多相关文章
- .NET Core单文件发布静态编译AOT CoreRT
.NET Core单文件发布静态编译AOT CoreRT,将.NET Core应用打包成一个可执行文件并包含运行时. 支持Windows, MacOS and Linux x64 w/ RyuJIT ...
- [转帖].NET Core单文件发布静态编译AOT CoreRT
.NET Core单文件发布静态编译AOT CoreRT https://www.cnblogs.com/linezero/p/CoreRT.htm .NET Core单文件发布静态编译AOT Cor ...
- .NET5.0 单文件发布打包操作深度剖析
.NET5.0 单文件发布打包操作深度剖析 前言 随着 .NET5.0 Preview 8 的发布,许多新功能正在被社区成员一一探索:这其中就包含了"单文件发布"这个炫酷的功能,实 ...
- DotnetCore 单文件发布
NETCORE3.0开始,可以发布单文件,参考https://www.cnblogs.com/ZaraNet/p/11790645.html 发布后(config目录 是手工复制进去的) 运行时, ...
- js和css文件位置对页面性能的影响
翻译了一篇Performance上的关于页面性能的文章<DecIPhering the critical rendering path>,原文在这里.需要进一步整理和了解有关js.css等 ...
- 高效编程之cache命中对于程序性能的影响
下面这个代码用两个双层循环遍历了一个二维数组里所有的元素,以我自己机器的测试 上面那个循环耗时基本为下面的一半,两个循环的时间复杂度相同,为什么会有这么大的差别? 首先要明白的是不管是几维数组,他们都 ...
- 随笔:关于.net core单文件AOT编译
.Net Core单文件发布已经很流畅了(vs已支持图形化操作发布). 但类似Go或者Graalvm JDK的完全事前编译为本地机器码的红能功能,还未发布于.net 6特性中,还处于实验室中. 另外, ...
- iOS 程序性能优化
前言 转载自:http://www.samirchen.com/ios-performance-optimization/ 程序性能优化不应该是一件放在功能完成之后的事,对性能的概念应该从我们一开始写 ...
- .Net 5下的单文件部署
由于.net程序没有静态链接,一直缺乏单文件部署这种干净的发布方案.对客户端程序发布并不是很友好.在之前的.net framework下,有ILMerge合并程序集,以及LibZ的嵌入资源文件等第三方 ...
随机推荐
- 钓鱼+DNS欺骗学习笔记
钓鱼+DNS欺骗学习笔记 0x00 写在前面 原文链接: http://www.cnblogs.com/hkleak/p/5186523.html 感谢大佬无私教学 0x01 步骤如下 第一步:布置钓 ...
- 如何解析EML(邮件)格式的文件以及一款小巧的EML邮件阅读工具
在理解EML格式的时候,先回顾一下历史,这样有助于理解邮件的格式,比如邮件传输时为何会有多种编码方式.此外,理解EML格式也有助于理解HTTP协议. 历史溯源 由于历史原因,我们目前看到的大部分的网络 ...
- 🍛 餐厅吃饭版理解 IO 模型:阻塞 / 非阻塞 / IO 复用 / 信号驱动 / 异步
IO 概念 一个基本的 IO,它会涉及到两个系统对象,一个是调用这个 IO 的进程对象,另一个就是系统内核 (kernel).当一个 read 操作发生时,它会经历两个阶段: 通过 read 系统调用 ...
- Lock 深入理解acquire和release原理源码及lock独有特性acquireInterruptibly和tryAcquireNanos
https://blog.csdn.net/sophia__yu/article/details/84313234 Lock是一个接口,通常会用ReentrantLock(可重入锁)来实现这个接口. ...
- Java使用多线程异步执行批量更新操作
import com.google.common.collect.Lists; import org.apache.commons.collections.CollectionUtils; impor ...
- mybatis中jdbcType和javaType
1.MyBatis 通过包含的jdbcType类型 BIT.FLOAT.CHAR .TIMESTAMP . OTHER .UNDEFINEDTINYINT .REAL .VARCHAR .BINARY ...
- Redis 集群,集群的原理是什么?
1).Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为master,继续提供服务. 2).Redis Cluster 着眼于扩展性,在单个 redis ...
- 用 wait-notify 写一段代码来解决生产者-消费者问题?
只要记住在同步块中调用 wait() 和 notify()方法,如 果阻塞,通过循环来测试等待条件.
- vue 初识(基础语法与数据驱动模型)
1.es6的语法 let 特点: 1.局部作用域 2.不会存在变量提升 3.变量不能重复声明 const 特点: 1.局部作用域 2.不会存在变量提升 3.不能重复声明,只声明常量 不可变的量 模板字 ...
- Java中的反射原理以及简单运用(原理+例子)
@ 目录 学习总结 1. 为什么要使用反射 2. 反射的概念 3. Java反射加载过程 4. 反射优缺点 5. 字节码对象理解 6. 获取字节码对象(.class)的三种方式 7. 反射常用API ...