Android 8.0 版本对 Android 运行时 (ART) 进行了重大改进。以下列表总结了设备制造商可以期望在 ART 中看到的增强功能。
并发压缩垃圾回收器
正如在 Google I/O 大会上宣布的那样,ART 在 Android 8.0 中推出了一种新的并发压缩垃圾回收器 (GC)。此回收器在每次 GC 运行时以及应用运行时压缩堆,仅在处理线程根目录时短暂暂停一次。以下是它的优势:
- GC 始终压缩堆:与 Android 7.0 相比,平均堆大小缩小 32%。
- 压缩支持线程本地 Bump Pointer 对象分配:分配速度比 Android 7.0 快 70%。
- 与 Android 7.0 GC 相比,H2 基准测试的暂停时间缩短了 85%。
- 暂停时间不再随堆大小而变化;应用应该能够使用大堆,而无需担心卡顿。
- GC 实现细节 - 读取屏障
- 读取屏障是每次读取对象字段时完成的少量工作。
- 这些在编译器中进行了优化,但可能会减慢某些用例的速度。
循环优化
ART 在 Android 8.0 版本中采用了各种循环优化:
- 边界检查消除
- 静态:范围在编译时被证明在边界内
- 动态:运行时测试确保循环保持在边界内(否则取消优化)
- 归纳变量消除
- 移除无效归纳
- 将仅在循环后使用的归纳替换为闭式表达式
- 循环体内部的死代码消除,删除变为无效的整个循环
- 强度缩减
- 循环转换:反转、互换、拆分、展开、单模等。
- SIMD 化(也称为向量化)
循环优化器位于 ART 编译器中自己的优化传递中。大多数循环优化类似于其他地方的优化和简化。挑战出现在一些以比通常更精细的方式重写 CFG 的优化中,因为大多数 CFG 实用程序(参见 nodes.h)侧重于构建 CFG,而不是重写 CFG。
类层次结构分析
Android 8.0 中的 ART 使用类层次结构分析 (CHA),这是一种编译器优化技术,它基于分析类层次结构生成的信息,将虚调用去虚化为直接调用。虚调用开销很大,因为它们围绕 vtable 查找实现,并且需要几个依赖加载。此外,虚调用无法内联。
以下是相关增强功能的摘要
- 动态单实现方法状态更新 - 在类链接时间结束时,当 vtable 已填充时,ART 会对超类的 vtable 进行逐条目比较。
- 编译器优化 - 编译器将利用方法的单实现信息。如果方法 A.foo 设置了单实现标志,编译器会将虚调用去虚化为直接调用,并进一步尝试内联直接调用。
- 已编译代码失效 - 同样在类链接时间结束时,当单实现信息更新时,如果之前具有单实现的方法 A.foo 的状态现在失效,则所有依赖于方法 A.foo 具有单实现的假设的已编译代码都需要使其已编译代码失效。
- 反优化 - 对于堆栈上的实时编译代码,将启动反优化以强制失效的编译代码进入解释器模式,以保证正确性。将使用一种新的反优化机制,它是同步和异步反优化的混合体。
内联缓存(在 .oat 文件中)
ART 现在采用内联缓存,并优化具有足够数据的调用站点。内联缓存功能将额外的运行时信息记录到配置文件中,并使用它将动态优化添加到提前编译中。
Dexlayout
Dexlayout 是 Android 8.0 中引入的一个库,用于分析 dex 文件并根据配置文件对其进行重新排序。Dexlayout 旨在利用运行时分析信息在设备上的空闲维护编译期间对 dex 文件的各个部分进行重新排序。通过将 dex 文件中经常一起访问的部分分组在一起,程序可以通过改进的局部性获得更好的内存访问模式,从而节省 RAM 并缩短启动时间。
由于配置文件信息目前仅在应用运行后可用,因此 dexlayout 集成在 dex2oat 的设备上空闲维护编译中。
Dex 缓存移除
在 Android 7.0 之前,DexCache 对象拥有四个大型数组,其大小与 DexFile 中某些元素的数量成正比,即
- 字符串(每个 DexFile::StringId 一个引用),
- 类型(每个 DexFile::TypeId 一个引用),
- 方法(每个 DexFile::MethodId 一个本机指针),
- 字段(每个 DexFile::FieldId 一个本机指针)。
这些数组用于快速检索我们之前解析的对象。在 Android 8.0 中,除了方法数组之外,所有数组都已移除。
解释器性能
在 Android 7.0 版本中,随着“mterp”(一种以汇编语言编写的核心提取/解码/解释机制为特色的解释器)的引入,解释器性能得到了显着提高。Mterp 模仿了快速 Dalvik 解释器,并支持 arm、arm64、x86、x86_64、mips 和 mips64。对于计算代码,Art 的 mterp 大致与 Dalvik 的快速解释器相当。但是,在某些情况下,它可能会显着甚至急剧地变慢
- 调用性能。
- 字符串操作以及其他大量使用在 Dalvik 中被识别为内在方法的方法。
- 更高的堆栈内存使用量。
Android 8.0 解决了这些问题。
更多内联
自 Android 6.0 以来,ART 可以内联同一 dex 文件中的任何调用,但只能内联来自不同 dex 文件的叶方法。此限制有两个原因
- 与同一 dex 文件内联不同,来自另一个 dex 文件的内联需要使用该另一个 dex 文件的 dex 缓存,后者可以重用调用者的 dex 缓存。在编译后的代码中,对于静态调用、字符串加载或类加载等几个指令,需要 dex 缓存。
- 堆栈映射仅编码当前 dex 文件中的方法索引。
为了解决这些限制,Android 8.0
- 从编译后的代码中移除 dex 缓存访问(另请参阅“Dex 缓存移除”部分)
- 扩展堆栈映射编码。
同步改进
ART 团队调整了 MonitorEnter/MonitorExit 代码路径,并减少了我们对 ARMv8 上传统内存屏障的依赖,尽可能用新的(acquire/release)指令替换它们。
更快的本机方法
使用 @FastNative
和 @CriticalNative
注解,可以使用更快的 Java 本地接口 (JNI) 本机调用。这些内置的 ART 运行时优化加速了 JNI 转换,并取代了现已弃用的!bang JNI 表示法。这些注解对非本机方法无效,并且仅适用于 bootclasspath
上的平台 Java 语言代码(没有 Play 商店更新)。
@FastNative
注解支持非静态方法。如果方法访问 jobject
作为参数或返回值,请使用此注解。
@CriticalNative
注解提供了一种更快的方式来运行本机方法,但有以下限制
- 方法必须是静态的 - 没有对象作为参数、返回值或隐式
this
。 - 只有原始类型会传递给本机方法。
- 本机方法在其函数定义中不使用
JNIEnv
和jclass
参数。 - 该方法必须使用
RegisterNatives
注册,而不是依赖于动态 JNI 链接。
@FastNative
可以将本机方法性能提高高达 3 倍,@CriticalNative
可以提高高达 5 倍。例如,在 Nexus 6P 设备上测量的 JNI 转换
Java 本地接口 (JNI) 调用 | 执行时间(纳秒) |
---|---|
常规 JNI | 115 |
!bang JNI | 60 |
@FastNative |
35 |
@CriticalNative |
25 |