Android 8.0 ART 改进

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 的快速解释器相当。但是,在某些情况下,它可能会显着甚至急剧地变慢

  1. 调用性能。
  2. 字符串操作以及其他大量使用在 Dalvik 中被识别为内在方法的方法。
  3. 更高的堆栈内存使用量。

Android 8.0 解决了这些问题。

更多内联

自 Android 6.0 以来,ART 可以内联同一 dex 文件中的任何调用,但只能内联来自不同 dex 文件的叶方法。此限制有两个原因

  1. 与同一 dex 文件内联不同,来自另一个 dex 文件的内联需要使用该另一个 dex 文件的 dex 缓存,后者可以重用调用者的 dex 缓存。在编译后的代码中,对于静态调用、字符串加载或类加载等几个指令,需要 dex 缓存。
  2. 堆栈映射仅编码当前 dex 文件中的方法索引。

为了解决这些限制,Android 8.0

  1. 从编译后的代码中移除 dex 缓存访问(另请参阅“Dex 缓存移除”部分)
  2. 扩展堆栈映射编码。

同步改进

ART 团队调整了 MonitorEnter/MonitorExit 代码路径,并减少了我们对 ARMv8 上传统内存屏障的依赖,尽可能用新的(acquire/release)指令替换它们。

更快的本机方法

使用 @FastNative@CriticalNative 注解,可以使用更快的 Java 本地接口 (JNI) 本机调用。这些内置的 ART 运行时优化加速了 JNI 转换,并取代了现已弃用的!bang JNI 表示法。这些注解对非本机方法无效,并且仅适用于 bootclasspath 上的平台 Java 语言代码(没有 Play 商店更新)。

@FastNative 注解支持非静态方法。如果方法访问 jobject 作为参数或返回值,请使用此注解。

@CriticalNative 注解提供了一种更快的方式来运行本机方法,但有以下限制

  • 方法必须是静态的 - 没有对象作为参数、返回值或隐式 this
  • 只有原始类型会传递给本机方法。
  • 本机方法在其函数定义中不使用 JNIEnvjclass 参数。
  • 该方法必须使用 RegisterNatives 注册,而不是依赖于动态 JNI 链接。

@FastNative 可以将本机方法性能提高高达 3 倍,@CriticalNative 可以提高高达 5 倍。例如,在 Nexus 6P 设备上测量的 JNI 转换

Java 本地接口 (JNI) 调用 执行时间(纳秒)
常规 JNI 115
!bang JNI 60
@FastNative 35
@CriticalNative 25