Android 运行时 (ART) 团队在不影响已编译代码或任何峰值内存回归的情况下,将编译时间缩短了 18%。这项改进是我们 2025 年计划的一部分,旨在缩短编译时间,同时不牺牲内存用量或编译后代码的质量。
优化编译时速度对于 ART 至关重要。例如,在进行即时 (JIT) 编译时,它会直接影响应用的效率和整体设备性能。编译速度越快,优化生效的时间就越短,从而带来更流畅、响应更迅速的用户体验。此外,对于 JIT 和预先 (AOT) 编译,编译时速度的提升意味着在编译过程中资源消耗的减少,从而延长电池续航时间并改善设备散热,尤其是在低端设备上。
其中一些编译时速度改进已在 2025 年 6 月的 Android 版本中推出,其余改进将在年底的 Android 版本中提供。此外,所有使用 Android 12 及更高版本的用户都可以通过 Mainline 更新获得这些改进。
优化优化编译器
优化编译器始终是一项需要权衡取舍的任务。您无法免费获得速度,必须放弃一些东西。我们为自己设定了一个非常明确且富有挑战性的目标:加快编译器速度,但不能引入内存回归,最重要的是,不能降低其生成的代码的质量。如果编译器速度更快,但应用运行速度更慢,我们就失败了。
我们愿意投入的唯一资源是我们自己的开发时间,以便深入挖掘、调查并找到符合这些严格标准的巧妙解决方案。让我们仔细看看我们如何寻找有待改进的领域,以及如何找到各种问题的正确解决方案。
寻找值得进行的潜在优化
在开始优化某个指标之前,您必须能够精准衡量该指标。否则,您将永远无法确定自己是否有所改进。幸运的是,只要您采取一些预防措施,例如在更改前后使用同一设备进行测量,并确保设备不会进入过热保护,编译时间速度就相当稳定。除此之外,我们还有编译器统计信息等确定性衡量指标,可帮助我们了解底层发生了什么情况。
由于我们为这些改进牺牲的资源是开发时间,因此我们希望能够尽可能快地进行迭代。这意味着,我们选取了一些具有代表性的应用(包括第一方应用、第三方应用和 Android 操作系统本身)来制作解决方案原型。后来,我们通过广泛的手动和自动测试验证了最终实现的价值。
有了这组精心挑选的 APK,我们就可以在本地触发手动编译,获取编译配置文件,并使用 pprof 可视化我们花费时间的位置。
pprof 中配置文件火焰图的示例
pprof 工具非常强大,可用于对数据进行切片、过滤和排序,以便查看哪些编译器阶段或方法占用了大部分时间。我们不会详细介绍 pprof 本身;您只需知道,如果条形图越大,则表示编译花费的时间越长。
其中一个视图是“自下而上”视图,您可以在其中查看哪些方法占用了大部分时间。在下图中,我们可以看到一个名为 Kill 的方法,该方法占编译时间的 1% 以上。本博文稍后还将讨论其他一些热门方法。
从底部向上查看个人资料
在我们的优化编译器中,有一个阶段称为全局值编号 (GVN)。您不必担心它作为一个整体的作用,但相关部分是了解它有一个名为“Kill”的方法,该方法会根据过滤条件删除一些节点。由于必须遍历所有节点并逐个检查,因此非常耗时。我们注意到,在某些情况下,无论当时有多少节点处于活跃状态,我们都能提前知道检查结果为 false。在这种情况下,我们可以完全跳过迭代,将该值从 1.023% 降至约 0.3%,并将 GVN 的运行时提高约 15%。
实施有价值的优化
我们介绍了如何衡量以及如何检测时间花费在何处,但这只是开始。下一步是了解如何优化编译所花费的时间。
通常,在上述 `Kill` 这样的情况下,我们会查看如何迭代节点,并通过例如并行处理或改进算法本身来加快迭代速度。事实上,我们一开始就是这样尝试的,只有在找不到任何可做的事情时,我们才突然意识到,解决方案是(在某些情况下)根本不进行迭代!在进行此类优化时,很容易只见树木不见森林。
在其他情况下,我们使用了多种不同的技术,包括:
- 使用启发法来决定优化是否会产生有价值的结果,因此可以跳过
- 使用额外的数据结构来缓存计算出的数据
- 更改当前数据结构以提高速度
- 延迟计算结果,以避免在某些情况下出现循环
- 使用合适的抽象层级 - 不必要的功能可能会降低代码速度
- 避免在多次加载中追逐常用指针
如何判断优化是否值得尝试?
妙就妙在,您无需这样做。在检测到某个区域消耗大量编译时间并投入开发时间尝试改进后,有时您可能找不到解决方案。也许是无事可做,也许是实现起来太费时间,也许是会显著降低其他指标,也许是会增加代码库的复杂性,等等。请注意,虽然您可以在这篇博文中看到许多成功的优化,但还有无数优化未能实现。
如果您也遇到类似情况,请尝试估计一下,通过尽可能少的努力,您能将指标提高多少。这意味着,按顺序:
- 根据已收集的指标或仅凭直觉进行估算
- 使用粗略的快速原型进行估算
- 实施解决方案。
别忘了考虑估算解决方案的缺点。例如,如果您要依赖额外的数据结构,您愿意使用多少内存?
深入了解
下面,我们来了解一下我们实施的一些变更。
我们实施了一项更改,以优化名为 FindReferenceInfoOf 的方法。此方法之前一直对向量执行线性搜索来查找条目。我们更新了该数据结构,使其按指令的 ID 进行索引,这样 FindReferenceInfoOf 的时间复杂度将为 O(1),而不是 O(n)。此外,我们还预先分配了向量,以避免调整大小。我们略微增加了内存,因为我们必须添加一个额外的字段来统计插入到向量中的条目数量,但这是一个很小的牺牲,因为峰值内存并没有增加。这使我们的 LoadStoreAnalysis 阶段加快了 34-66%,从而使编译时间缩短了约 0.5-1.8%。
我们在多个位置使用了 HashSet 的自定义实现。创建此数据结构需要相当长的时间,我们找到了原因。多年前,此数据结构仅用于少数使用非常大的 HashSet 的位置,并经过调整以针对这些位置进行优化。不过,如今它被反向使用,只有少数条目,且生命周期较短。这意味着,我们创建这个巨大的 HashSet 浪费了周期,但我们只使用了其中的几个条目,然后就将其舍弃了。通过此变更,我们将编译时间缩短了约 1.3-2%。此外,由于我们不再使用之前那么大的数据结构,内存使用量减少了约 0.5-1%。
我们通过按引用将数据结构传递给 lambda,避免了复制这些数据结构,从而将编译时间缩短了大约 0.5-1%。这是原始审核中遗漏的问题,在我们的代码库中存在了多年。正是通过查看 pprof 中的配置文件,我们才注意到这些方法正在创建和销毁大量数据结构,这促使我们对其进行调查和优化。
我们通过缓存计算值加快了写入编译后输出的阶段,这相当于将总编译时间缩短了约 1.3-2.8%。遗憾的是,额外的簿记工作量过大,我们的自动化测试也提醒我们出现了内存回归。后来,我们再次查看了同一段代码,并实现了新版本,该版本不仅解决了内存回归问题,还进一步缩短了编译时间,缩短幅度约为 0.5-1.8%!在第二次更改中,我们必须重构并重新构想此阶段应如何运作,才能摆脱两种数据结构中的一种。
在优化编译器中,我们有一个阶段会内联函数调用,以获得更好的性能。为了选择要内联的方法,我们在进行任何计算之前使用启发式方法,并在完成工作后但在最终确定内联之前进行最终检查。如果其中任何一个检测到内联不值得(例如,会添加过多的新指令),那么我们就不会内联方法调用。
我们将两项检查从“最终检查”类别移至“启发式”类别,以便在进行任何耗时的计算之前估计内嵌是否会成功。由于这只是一个估计值,因此并不完美,但我们已验证,新的启发式方法涵盖了之前内嵌的 99.9% 的内容,且不会影响性能。其中一种新的启发式方法与所需的 DEX 寄存器(改进幅度约为 0.2-1.3%)有关,另一种与指令数量(改进幅度约为 2%)有关。
我们有一个在多个位置使用的 BitVector 自定义实现。我们已将可调整大小的 BitVector 类替换为更简单的 BitVectorView,用于某些固定大小的位向量。这样可以消除一些间接寻址和运行时范围检查,并加快位向量对象的构建速度。
此外,BitVectorView 类已根据底层存储类型进行模板化(而不是像旧版 BitVector 那样始终使用 uint32_t)。这样一来,某些操作(例如 Union())便可在 64 位平台上同时处理两倍的位。在编译 Android OS 时,受影响函数的样本总数减少了 1% 以上。此操作是通过多次更改完成的 [1、2、3、4、5、6]
如果我们详细讨论所有优化,那可能要在这里待上一整天!如果您对更多优化感兴趣,请查看我们实施的其他一些更改:
- 添加了簿记,可将编译时间缩短约 0.6-1.6%。
- 尽可能延迟计算数据,以避免出现循环。
- 重构代码,以便在不使用预计算工作时跳过该工作。
- 当可以从其他位置轻松获取分配器时,避免某些依赖性负载链。
- 另一个添加检查以避免不必要的工作的示例。
- 避免在寄存器分配器中频繁对寄存器类型(核心/FP)进行分支。
- 确保在编译时初始化某些数组。不要依赖 clang 来执行此操作。
- 清理一些循环。使用 clang 可以更好地优化的范围循环,因为循环副作用不需要重新加载容器的内部指针。避免通过内嵌的 `InputAt(.)` 为每个输入在循环中调用虚拟函数 `HInstruction::GetInputRecords()`。
- 通过利用编译器优化,避免为访问者模式使用 Accept() 函数。
总结
我们一直致力于提高 ART 的编译时速度,并取得了显著的改进,这不仅使 Android 更加流畅高效,还有助于延长电池续航时间并改善设备散热性能。通过认真识别和实施优化,我们证明了在不影响内存使用量或代码质量的情况下,可以大幅缩短编译时间。
我们的历程包括使用 pprof 等工具进行分析、愿意迭代,有时甚至会放弃不太有成效的途径。ART 团队的共同努力不仅显著缩短了编译时间,还为未来的改进奠定了基础。
所有这些改进都将在 2025 年年底的 Android 更新中提供,并且可通过 Mainline 更新在 Android 12 及更高版本中提供。我们希望通过深入了解我们的优化流程,您能对编译器工程的复杂性和回报有更深入的了解!
继续阅读
-
产品资讯
每位开发者的 AI 工作流程和需求都是独一无二的,因此能够选择 AI 如何帮助您进行开发非常重要。1 月,我们推出了在 Android Studio 中选择任何本地或远程 AI 模型来支持 AI 功能的功能
Matthew Warner • 阅读用时:2 分钟
-
产品资讯
Android Studio Panda 3 现已是稳定版,可在生产环境中使用。此版本可让您对 AI 赋能的工作流程进行更多控制和自定义,从而比以往更轻松地构建高品质的 Android 应用。
Matt Dyor • 阅读用时:3 分钟
-
产品资讯
Google 致力于将最强大的 AI 模型直接引入您口袋中的 Android 设备。今天,我们非常高兴地宣布推出最新的领先开放模型:Gemma 4。
Caren Chang, David Chou • 阅读用时:3 分钟
随时了解最新动态
每周通过电子邮件接收最新的 Android 开发洞见。