JIT- 动态编译与 AOT- 静态编译:java/ java/ JavaScript/Dart 乱谈
C 和 C++ 之类的编译语言性能远超 Java,但是生成的代码只能在有限的几种系统上执行,这就有了 Java 的存在基础(JVM-跨平台)
早期 Java 运行时所提供的性能级别远低于 C 和 C++ 之类的编译语言。
最早的时候,java 是由解释器(Interpreter),将每个 java 指令转译为对等的微处理器指令,并根据转译后的指令先后次序依序执行,一个 java 指令可能对应十几或者几十个对等微处理指令,运行的时候还要先解释,在硬件条件差的情况下,执行速度是可想而知有多慢的
后面 Java 通过 JIT 编译器(Just-in-time Compiler) 优化,开挂霸占 Web 开发头牌几十年。比如傍上 java 这个亲戚的 JavaScript,在 V9 引擎里通过 JIT,造成前端 吼吼 Web 一条龙服务(nodeJS 全栈),感兴趣可以看下《ECMAScript进化史(1):话说Web脚本语言王者JavaScript的加冕历史》
当 java 执行 runtime 环境时,每遇到一个 class,JIT 就会对这个类进行编译,生成相当精简的二进制码,花费少许的编译时间来换取后续的执行速率,这个效率提高还是比较大的,但这并没有达到顶尖的效能,因为某些 java 文件是极少执行的,编译它们的时间有可能远远长于转译器转译执行的时间,整体下来,花费的时间并没有减少。
基于 JIT 的经验,又出来了动态编译器(dynamic compiler),动态预判哪些需要 compile 哪些需要转译,所以动态编译器是既包含了转译器 &编译器的。
JIT 动态编译
尽管传闻中 Java 编程的 “一次编写,随处运行” 的口号可能并非在所有情况下都严格成立,但是对于大量的应用程序来说情况确实如此。另一方面,本地编译本质上是特定于平台的。
那么 Java 平台如何在不牺牲平台无关性的情况下实现本地编译的性能?答案就是使用 JIT 编译器进行动态编译,这种方法已经使用了十年
尽管通过 JIT 编译保持了平台无关性,但是付出了一定代价。因为在程序执行时进行编译,所以编译代码的时间将计入程序的执行时间。任何编写过大型 C 或 C++ 程序的人都知道,编译过程往往较慢。
克服编译过程慢
编译所有的代码,但是不执行任何耗时多的分析和转换,因此可以快速生成代码。由于生成代码的速度很快,因此尽管可以明显观察到编译带来的开销,但是这很容易就被反复执行本地代码所带来的性能改善所掩盖。
将编译资源只分配给少量的频繁执行的方法(通常称作热方法)。低编译开销更容易被反复执行热代码带来的性能优势掩盖。很多应用程序只执行少量的热方法,因此这种方法有效地实现了编译性能成本的最小化。
动态编译器的一个主要的复杂性在于权衡了解编译代码的预期获益使方法的执行对整个程序的性能起多大作用。一个极端的例子是,程序执行后,您非常清楚哪些方法对于这个特定的执行的性能贡献最大,但是编译这些方法毫无用处,因为程序已经完成。而在另一个极端,程序执行前无法得知哪些方法重要,但是每种方法的潜在受益都最大化了。大多数动态编译器的操作介于这两个极端之间,方法是权衡了解方法预期获益的重要程度。
Java 语言需要动态加载类这一事实对 Java 编译器的设计有着重要的影响。如果待编译代码引用的其他类还没有加载怎么办?
比如一个方法需要读取某个尚未加载的类的静态字段值。Java 语言要求第一次执行类引用时加载这个类并将其解析到当前的 JVM 中。直到第一次执行时才解析引用,这意味着没有地址可供从中加载该静态字段。
编译器如何处理这种可能性?
编译器生成一些代码,用于在没有加载类时加载并解析类。类一旦被解析,就会以一种线程安全的方式修改原始代码位置以便直接访问静态字段的地址,因为此时已获知该地址。
IBM JIT 编译器中进行了大量的努力以便使用安全而有效率的代码补丁技术,因此在解析类之后,执行的本地代码只加载字段的值,就像编译时已经解析了字段一样。另外一种方法是生成一些代码,用于在查明字段的位置以前一直检查是否已经解析字段,然后加载该值。对于那些由未解析变成已解析并被频繁访问的字段来说,这种简单的过程可能带来严重的性能问题。
动态编译的优/缺点
动态地编译 Java 程序有一些重要的优点,甚至能够比静态编译语言更好地生成代码,现代的 JIT 编译器常常向生成的代码中插入挂钩以收集有关程序行为的信息,以便如果要选择方法进行重编译,就可以更好地优化动态行为。
但是,动态编译确实具有一些缺点,这些缺点使它在某些情况下算不上一个理想的解决方案
因为识别频繁执行的方法以及编译这些方法需要时间,所以应用程序通常要经历一个准备过程,在这个过程中性能无法达到其最高值。
在这个准备过程中出现性能问题有几个原因:
首先,大量的初始编译可能直接影响应用程序的启动时间。不仅这些编译延迟了应用程序达到稳定状态的时间(想像 Web 服务器经历一个初始阶段后才能够执行实际有用的工作),而且在准备阶段中频繁执行的方法可能对应用程序的稳定状态的性能所起的作用也不大。如果 JIT 编译会延迟启动又不能显著改善应用程序的长期性能,则执行这种编译就非常浪费。虽然所有的现代 JVM 都执行调优来减轻启动延迟,但是并非在所有情况下都能够完全解决这个问题。
有些应用程序完全不能忍受动态编译带来的延迟。如 GUI 接口之类交互式应用程序就是这样的例子。在这种情况下,编译活动可能对用户使用造成不利影响,同时又不能显著地改善应用程序的性能。
最后,用于实时环境并具有严格的任务时限的应用程序可能无法忍受编译的不确定性性能影响或动态编译器本身的内存开销。
因此,虽然 JIT 编译技术已经能够提供与静态语言性能相当(甚至更好)的性能水平,但是动态编译并不适合于某些应用程序。在这些情况下,Java 代码的提前(Ahead-of-time,AOT)编译可能是合适的解决方案。
AOT 提前编译
动态类加载是动态 JIT 编译器面临的一个挑战,也是 AOT 编译的一个更重要的问题。只有在执行代码引用类的时候才加载该类。因为是在程序执行前进行 AOT 编译的,所以编译器无法预测加载了哪些类。就是说编译器无法获知任何静态字段的地址、任何对象的任何实例字段的偏移量或任何调用的实际目标,甚至对直接调用(非虚调用)也是如此。在执行代码时,如果证明对任何这类信息的预测是错误的,这意味着代码是错误的并且还牺牲了 Java 的一致性。
因为代码可以在任何环境中执行,所以类文件可能与代码编译时不同。例如,一个 JVM 实例可能从磁盘的某个特定位置加载类,而后面一个实例可能从不同的位置甚至网络加载该类。设想一个正在进行 bug 修复的开发环境:类文件的内容可能随不同的应用程序的执行而变化。此外,Java 代码可能在程序执行前根本不存在:比如 Java 反射服务通常在运行时生成新类来支持程序的行为。
缺少关于静态、字段、类和方法的信息意味着严重限制了 Java 编译器中优化框架的大部分功能。内联可能是静态或动态编译器应用的最重要的优化,但是由于编译器无法获知调用的目标方法,因此无法再使用这种优化。
JIT vs JIT
JIT:吞吐量高,有运行时性能加成,可以跑得更快,并可以做到动态生成代码等,但是相对启动速度较慢,并需要一定时间和调用频率才能触发 JIT 的分层机制
AOT:内存占用低,启动速度快,可以无需 runtime 运行,直接将 runtime 静态链接至最终的程序中,但是无运行时性能加成,不能根据程序运行情况做进一步的优化
看了轮子哥的回答:
AOT 是事先生成机器码,其实就跟 C++这样的语言差不多。选择这么做通常都会意味着你损失了一个功能——譬如说
C#的【虚函数也可以是模板函数】功能啦;
【用反射就地组合成新模板类(你有 List<>,有 int,代码里面没出现过 List<int>,你也可以 new 出来,C++怎么做都不行的)】功能啦;
【class Fuck<T>{public Fuck<Fuck<T>> Shit{get;set;}}(C++这么干编译器会傻逼啊哈哈哈)】功能啦;
所有这些功能都要求你必须运行到那才产生机器码的。
JIT 还有一个好处就是做 profiling based optimization 方便。当然,这样就使得运行的时候会稍微慢一点点,不过这一点点是人类不可察觉的。
AOT 的缺陷
应用安装和系统升级之后的应用优化比较耗时(重新编译,把程序代码转换成机器语言)
优化后的文件会占用额外的存储空间(缓存转换结果)
总结来讲:
在开发期使用 JIT 编译,可以缩短产品的开发周期。Flutter 最受欢迎的功能之一热重载,正是基于此特 性。
在发布期使用 AOT,就不需要像 React Native 那样在跨平台 JavaScript 代码和原生 Android、iOS 代码之间建立低效的方法调用映射关系。
JIT 和 AOT 共存
JIT 与 AOT 各有千秋,两者融合,比如大火的多端一体化 Flutter+Dart,其实不光做做客户端咯,服务端应用有各自不同的运行特点,Dart 能够更好地适配。
Dart
Dart 是少数同时支持 JIT(Just In Time,即时编译)和 AOT(Ahead of Time,运行前编译)的语言之一。
Dart 吸取了其它高级语言设计的精华,例如 Smalltalk 的 Image 技术、JVM 的 HotSpot 和 Dart 编译技术又师出同门。由 Dart 实现的语言容器,它可以在启动速度、运行性能有不错的表现。Dart 提供了 AoT、JIT 的编译方式,JIT 拥有 Kernel 和 AppJIT 的运行模式
dart 优势
Dart 在开发过程中使用 JIT,因此每次改都不需要再编译成字节码。节省了大量时间。
在部署中使用 AOT 生成高效的 ARM 代码以保证高效的性能。
JIT 在运行时即时编译,在开发周期中使用,可以动态下发和执行代码,开发测试效率高,但运行速度和执行性能则会因为运行时即时编译受到影响。
所以说,Dart 具有运行速 度快、执行性能好的特点。
Android
Android 7.0 上,JIT 编译器被再次使用,采用 AOT/JIT 混合编译的策略,特点是:
应用在安装的时候 dex 不会再被编译
App 运行时,dex 文件先通过解析器被直接执行,热点函数会被识别并被 JIT 编译后存储在 jit code cache 中并生成 profile 文件以记录热点函数的信息。
手机进入 IDLE(空闲) 或者 Charging(充电) 状态的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。
Dalvik,ART 是 Android 的两种运行环境,也可以叫做 Android 虚拟机 JIT,AOT 是 Android 虚拟机采用的两种不同的编译策略
参考内容:
浅谈 JIT&AOT https://www.jianshu.com/p/ac079e7fc412
JIT(动态编译)和 AOT(静态编译)编译技术比较 https://www.cnblogs.com/tinytiny/p/3200448.html
说一说 Android 的 Dalvik,ART 与 JIT,AOT https://zhuanlan.zhihu.com/p/53723652
对比 JIT 和 AOT,各自有什么优点与缺点? - hez2010 的回答 - 知乎 https://www.zhihu.com/question/23874627/answer/950484956
对比 JIT 和 AOT,各自有什么优点与缺点? - 圆胖肿的回答 - 知乎 https://www.zhihu.com/question/23874627/answer/889699901
转载本站文章《JIT-动态编译与AOT-静态编译:java/ java/ JavaScript/Dart乱谈》,请注明出处:https://www.zhoulujun.cn/html/theory/ComputerScienceTechnology/Compiling/2020_0714_8513.html
版权声明: 本文为 InfoQ 作者【zhoulujun】的原创文章。
原文链接:【http://xie.infoq.cn/article/887636522bd07991b333b1a7c】。文章转载请联系作者。
评论