blob: 9e762394868338fe119089c181aeb16976ae387e [file] [log] [blame]
<html devsite><head>
<title>Android 8.0 中的 ART 功能改进</title>
<meta name="project_path" value="/_project.yaml"/>
<meta name="book_path" value="/_book.yaml"/>
</head>
<body>
<!--
Copyright 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<p>
在 Android 8.0 版本中,Android Runtime (ART) 有了极大改进。下面的列表总结了设备制造商可以在 ART 中获得的增强功能。
</p>
<h2 id="concurrent-compacting-gc">并发压缩式垃圾回收器</h2>
<p>
正如 Google 在 Google I/O 大会上所宣布的那样,ART 在 Android 8.0 中提供了新的并发压缩式垃圾回收器 (GC)。该回收器会在每次执行 GC 时以及应用正在运行时对堆进行压缩,且仅在处理线程根时短暂停顿一次。该回收器具有以下优势:
</p>
<ul>
<li>
GC 始终会对堆进行压缩:堆的大小平均比 Android 7.0 中的小 32%。
</li>
<li>
得益于压缩,系统现可实现线程局部碰撞指针对象分配:分配速度比 Android 7.0 中的快 70%。
</li>
<li>
H2 基准的停顿次数比 Android 7.0 GC 的少 85%。
</li>
<li>
停顿次数不再随堆的大小而变化,应用在使用较大的堆时也无需担心造成卡顿。
</li>
<li>GC 实现细节 - 读取屏障:
<ul>
<li>
读取屏障是在读取每个对象字段时所做的少量工作。
</li>
<li>
它们在编译器中经过了优化,但可能会减慢某些用例的速度。
</li>
</ul>
</li></ul>
<h2 id="loop-optimizations">循环优化</h2>
<p>
在 Android 8.0 版本中,ART 采取了多种循环优化措施,具体如下:
</p>
<ul>
<li>消除边界检查
<ul>
<li>静态:在编译时证明范围位于边界内</li>
<li>
动态:运行时测试确保循环始终位于边界内(否则不进行优化)
</li>
</ul>
</li>
<li>消除归纳变量
<ul>
<li>移除无用归纳</li>
<li>
用封闭式表达式替换仅在循环后使用的归纳
</li>
</ul>
</li>
<li>
消除循环主体内的无用代码,移除整个死循环
</li>
<li>强度降低</li>
<li>
循环转换:逆转、交换、拆分、展开、单模等
</li>
<li>SIMDization(也称为矢量化)</li>
</ul>
<p>
循环优化器位于 ART 编译器中一个独立的优化环节中。大多数循环优化与其他方面的优化和简化类似。由于大多数 CFG 实用工具(请参阅 nodes.h)侧重于编译而不是重写 CFG,因此通过更复杂(与平时相比)的方式进行一些重写 CFG 的优化时,会面临挑战。
</p>
<h2 id="class-hierarchy-analysis">类层次结构分析</h2>
<p>
在 Android 8.0 中,ART 会使用类层次结构分析 (CHA),这是一种编译器优化,可基于通过分析类层次结构所生成的信息,将虚拟调用去虚拟化为直接调用。虚拟调用代价高昂,因为它们围绕 vtable 查找来实现,且会占用几个依赖负载。另外,虚拟调用也不能内嵌。
</p>
<p>以下是对相关增强功能的总结:</p>
<ul>
<li>
动态单一实现方法状态更新 - 在类关联时间结束时,如果 vtable 已被填充,ART 会按条目对超类的 vtable 进行比较。
</li>
<li>编译器优化 - 编译器会利用某种方法的单一实现信息。如果方法 A.foo 设置了单一实现标记,则编译器会将虚拟调用去虚拟化为直接调用,并借此进一步尝试内嵌直接调用。
</li>
<li>
已编译代码无效 - 另外,在类关联时间结束时,如果单一实现信息进行了更新,且方法 A.foo 之前拥有单一实现,但该状态现已变为无效,则依赖方法 A.foo 拥有单一实现这一假设的所有已编译代码都需要变为无效代码。
</li>
<li>
去优化 - 对于堆栈上已编译的有效代码,系统会启动去优化功能,以强制使已编译无效代码进入解释器模式,从而确保正确性。系统会采用结合了同步和异步去优化的全新去优化机制。
</li>
</ul>
<h2 id="inline-caches-in-oat-files">.oat 文件中的内嵌缓存</h2>
<p>
ART 现在采用内嵌缓存,并对有足够数据可用的调用站点进行优化。内嵌缓存功能会将额外的运行时信息记录到配置文件中,并利用这类信息将动态优化添加到预先编译中。
</p>
<h2 id="dexlayout">Dexlayout</h2>
<p>
Dexlayout 是在 Android 8.0 中引入的一个库,用于分析 dex 文件,并根据配置文件对其进行重新排序。Dexlayout 旨在使用运行时配置信息对 dex 文件的各个部分进行重新排序(在设备的空闲维护编译期间)。通过将经常一起访问的部分 dex 文件集中在一起,程序可以因改进文件位置而拥有更好的内存访问模式,从而节省 RAM 并缩短启动时间。
</p>
<p>
由于配置文件信息目前仅在运行应用后可用,因此系统会在空闲维护期间将 dexlayout 集成到 dex2oat 的设备编译中。
</p>
<h2 id="dex-cache-removal">Dex 缓存移除</h2>
<p>
在 Android 7.0 及更早版本中,DexCache 对象拥有四个大型数组,与 DexFile 中特定元素的数量成正比,即:
</p>
<ul>
<li>
字符串(每个 DexFile::StringId 一个引用),
</li>
<li>
类型(每个 DexFile::TypeId 一个引用),
</li>
<li>
方法(每个 DexFile::MethodId 一个原生指针),
</li>
<li>
字段(每个 DexFile::FieldId 一个原生指针)。
</li>
</ul>
<p>
这些数组用于快速检索我们以前解析的对象。在 Android 8.0 中,除方法数组外,所有数组都已移除。
</p>
<h2 id="interpreter-performance">解释器性能</h2>
<p>
通过引入 Mterp(一种解释器,具有以汇编语言编写的核心提取/解码/解释机制),Android 7.0 版本中的解释器性能得以显著提升。Mterp 模仿了快速 Dalvik 解释器,并支持 arm、arm64、x86、x86_64、mips 和 mips64。对于计算代码而言,ART 的 Mterp 大致相当于 Dalvik 的快速解释器。不过,有时候,它的速度可能会显著变慢,甚至急剧变慢:
</p>
<ol>
<li>调用性能。</li>
<li>
字符串操作和 Dalvik 中其他被视为内嵌函数的高频用户方法。
</li>
<li>堆栈内存使用量较高。</li>
</ol>
<p>Android 8.0 解决了这些问题。</p>
<h2 id="more-inlining">详细了解内嵌</h2>
<p>
从 Android 6.0 开始,ART 可以内嵌同一个 dex 文件中的任何调用,但只能内嵌来自其他 dex 文件的叶方法。此项限制具有以下两个原因:
</p>
<ol>
<li>
从其他 dex 文件进行内嵌需要使用该 dex 文件的 dex 缓存,这与同一 dex 文件内嵌(只能重复使用调用程序的 dex 缓存)有所不同。已编译代码中需要具有 dex 缓存,以便执行一系列指令,例如静态调用、字符串加载或类加载。
</li><li>
堆栈映射只对当前 dex 文件中的方法索引进行编码。
</li>
</ol>
<p>为了应对这些限制,Android 8.0 做出了以下改进:</p>
<ol>
<li>从已编译代码中移除 dex 缓存访问(另请参阅“Dex 缓存移除”部分)</li>
<li>扩展堆栈映射编码。</li>
</ol>
<h2 id="synchronization-improvements">同步方面的改进</h2>
<p>
ART 团队调整了 MonitorEnter/MonitorExit 代码路径,并减少了我们对 ARMv8 上传统内存屏障的依赖,尽可能将其替换为较新的(获取/释放)指令。
</p>
<h2 id="faster-native-methods">更快速的原生方法</h2>
<p>
使用 <a class="external" href="https://android.googlesource.com/platform/libcore/+/master/dalvik/src/main/java/dalvik/annotation/optimization/FastNative.java"><code>@FastNative</code></a><a class="external" href="https://android.googlesource.com/platform/libcore/+/master/dalvik/src/main/java/dalvik/annotation/optimization/CriticalNative.java"><code>@CriticalNative</code></a> 注解可以更快速地对 Java 原生接口 (JNI) 进行原生调用。这些内置的 ART 运行时优化可以加快 JNI 转换速度,并取代了现在已弃用的 !<em></em>bang JNI 标记。注解对非原生方法没有任何影响,并且仅适用于 <code>bootclasspath</code> 上的平台 Java 语言代码(无 Play 商店更新)。
</p>
<p>
<code>@FastNative</code> 注解支持非静态方法。如果某种方法将 <code>jobject</code> 作为参数或返回值进行访问,请使用此注解。
</p>
<p>
利用 <code>@CriticalNative</code> 注解,可更快速地运行原生方法,但存在以下限制:
</p>
<ul>
<li>
方法必须是静态的 - 没有参数、返回值或隐式 <code>this</code> 的对象。
</li>
<li>仅将基元类型传递给原生方法。</li>
<li>
原生方法在其函数定义中不使用 <code>JNIEnv</code><code>jclass</code> 参数。
</li>
<li>
该方法必须是使用 <code>RegisterNatives</code> 注册的,而不是依靠动态 JNI 链接。
</li>
</ul>
<aside class="caution">
<p>
<code>@FastNative</code><code>@CriticalNative</code> 注解在执行原生方法时会停用垃圾回收。请勿使用运行时间较长的方法,包括通常很快但一般不受控制的方法。
</p>
<p>
停顿垃圾回收可能会导致死锁。如果锁尚未得到本地释放(即尚未返回受管理代码),请勿在原生快速调用期间获取锁。此要求不适用于常规的 JNI 调用,因为 ART 将正执行的原生代码视为已暂停的状态。
</p>
</aside>
<p>
<code>@FastNative</code> 可以使原生方法的性能提升高达 2 倍,<code>@CriticalNative</code> 则可以提升高达 4 倍。例如,在 Nexus 6P 设备上测量的 JNI 转换如下:
</p>
<table>
<tbody><tr>
<th>Java 原生接口 (JNI) 调用</th>
<th>执行时间(以纳秒计)</th>
</tr>
<tr>
<td>常规 JNI</td>
<td>115</td>
</tr>
<tr>
<td><em>!bang JNI</em></td>
<td>60</td>
</tr>
<tr>
<td><code>@FastNative</code></td>
<td>35</td>
</tr>
<tr>
<td><code>@CriticalNative</code></td>
<td>25</td>
</tr>
</tbody></table>
</body></html>