从快照运行
VM 能够将 isolate
的堆,或位于堆中的更精确地序列化对象的图称为二进制快照,然后可以使用快照在启动 VM isolates 时重新创建相同的状态。
快照的格式是底层的,并且针对快速启动进行了优化:「它本质上是一个要创建的对象列表以及有关如何将它们连接在一起的说明」。
快照背后的最初想法:VM 无需解析 Dart 源和逐步创建内部 VM 数据结构,而是可以将所有必要的数据结构从快照中快速解包出来,然后进行 isolate
up。
❝
快照的想法源于 Smalltalk 图像,而后者又受到 Alan Kay 的硕士论文的启发。Dart VM 使用集群序列化格式,这类似于 《Parcels: a Fast and Feature-Rich Binary Deployment Technology》和《Clustered serialization with Fuel》论文中描述的技术。
❞
最初快照不包括机器代码,但是后来在开发 AOT 编译器时添加了此功能。开发 AOT 编译器和带有代码的快照的动机:「是为了允许在由于平台级别限制而无法进行 JIT 的平台上使用 VM」。
带有代码的快照的工作方式几乎与普通快照相同,但有细微差别:它们包含一个代码部分,这部分与快照的其余部分不同,它不需要反序列化,此代码部分的放置方式允许它在映射到内存后直接成为堆的一部分。
❝
runtime/vm/clustered_snapshot.cc
处理快照的序列化和反序列化;API 函数Dart_CreateXyzSnapshot[AsAssembly]
负责写出堆的快照(例如Dart_CreateAppJITSnapshotAsBlobs
和Dart_CreateAppAOTSnapshotAssembly
);Dart_CreateIsolateGroup
可选择获取快照数据以启动isolate
。❞
从 AppJIT 快照运行
「引入 AppJIT 快照是为了减少大型 Dart 应用程序的 JIT 预热时间」,例如 dartanalyzer
或 dart2js
。当这些工具用于小型项目时,它们花在实际工作上的时间与 VM 花在 JIT 编译这些应用程序上的时间一样多。
AppJIT 快照可以解决这个问题:可以使用一些模拟训练数据在 VM 上运行应用程序,然后将所有生成的代码和 VM 内部数据结构序列化为 AppJIT 快照,然后分发此快照,而不是以源(或内核二进制)形式分发应用程序。
从这个快照开始的 VM 仍然可以 JIT。
从 AppAOT 快照运行
AOT 快照最初是为无法进行 JIT 编译的平台引入的,但它们也可用于快速启动和更低性能损失的情况。
关于 JIT 和 AOT 的性能特征比较通常存在很多混淆的概念:
- JIT 可以访问正在运行的应用程序的本地类型信息和执行配置文件,但是它必须为预热付出代价;
- AOT 可以在全局范围内推断和证明各种属性(为此它必须支付编译时间),没有关于程序实际执行方式的信息, 但 AOT 编译代码几乎立即达到其峰值性能,几乎没有任何预热.
❝
目前 Dart VM JIT 的峰值性能最好,而 Dart VM AOT 的启动时间最好。
❞
无法进行 JIT 意味着:
- 1、AOT 快照必须包含可以在应用程序执行期间调用的每个函数的可执行代码;
- 2、可执行代码不得依赖任何可能在执行过程中会被违反的推测性假设;
为了满足这些要求,AOT 编译过程会进行全局静态分析(类型流分析或TFA),以确定应用程序的哪些部分可以从已知的入口点集合、分配哪些类的实例,以及类型如何在程序运转。
所有这些分析都是保守的:意味着它们在没办法和 JIT 一样执行更多的优化执行,因为它总是可以反优化为未优化的代码以实现正确的行为。
所有可能用到的函数都会被编译为本机代码,无需任何推测优化,而类型流信息仍然用专门代码处理(例如去虚拟化调用)。
编译完所有函数后,就可以拍摄堆的快照,然后就可以使用预编译运行时运行生成的快照,这是 Dart VM 的一种特殊变体,它不包括 JIT 和动态代码加载工具等组件。
❝
package:vm/transformations/type_flow/transformer.dart
是基于 TFA 结果的类型流分析和转换的入口点;dart::Precompiler::DoCompileAll
是 VM 中 AOT 编译循环的入口点。❞
可切换调用
即使进行了全局和局部分析,AOT 编译代码仍可能包含无法去虚拟化的调用(意味着它们无法静态解析)。为了补偿这种 AOT 编译代码,运行时使用 JIT 中的内联缓存技术扩展,此扩展版本称为 switchable calls
。
JIT 部分已经描述了与调用点关联的每个内联缓存由两部分组成:
- 缓存对象(由
dart::UntaggedICData
实例表示); - 要调用的本地代码块(例如
InlineCacheStub
);
在 JIT 模式下,运行时只会更新缓存本身,但是在 AOT 运行时可以根据内联缓存的状态选择替换缓存和要调用的本机代码。
最初所有动态调用都以未链接状态开始,当达到第一次调用点 SwitchableCallMissStub
被调用时,它只是调用到运行帮手 DRT_SwitchableCallMiss
链接该调用位置。
之后 DRT_SwitchableCallMiss
会尝试将呼叫点转换为单态状态,在这种状态下调用点变成了直接调用,它通过一个特殊的入口点进入方法,该入口点验证接收者是否具有预期的类。
在上面的示例中,我们假设 obj.method()
第一次执行的实例是 C
, 并 obj.method
解析为 C.method
。
下次我们执行相同的调用点时,它将 C.method
直接调用,绕过任何类型的方法查找过程。
但是它会将 C.method
通过一个特殊的入口点进入,这将验证它 obj
仍然是 C
, 如果不是这种情况,将调用 DRT_SwitchableCallMiss
并尝试选择下一个呼叫点状态。
C.method
可能仍然是调用的有效目标,例如 obj
是 D extends
C , 但不覆盖的类的实例 C.method
,在这种情况下,我们会检查调用点是否可以转换为单个目标状态,由 SingleTargetCallStub
实现(另见 dart::UntaggedSingleTargetCache
)。