1. 概述
近日,Android官方宣布了一项引人瞩目的技术成果:Android Runtime(ART)的编译速度实现了18%的显著提升!
同时,还做到了:
- 没有牺牲编译代码的质量,即对编译器最终输出质量没有负面影响
- 没有出现内存回归,即提升过程中不增加内存占用峰值
2. 优化意义是什么?
即做了这次优化后,体现在哪些方面?
Android Runtime(ART)编译的速度不是【开发者用 Gradle 编译构建 APK 的速度】,而是【设备端 ART 对 字节码进行 AOT / JIT 编译的速度】。所以这次的提升效率是用户显而易见的:
- 【更快的】应用冷启动、更【流程、少卡顿】:编译速度提升意味着应用(尤其是冷启动时)的代码能更快地被编译为高效机器码,从而减少启动等待时间和运行中的卡顿。
- 【更快的】安装与更新流程:无论是新应用安装还是系统OTA更新,对字节码的编译处理环节都会更快,使得整个流程更加顺畅。
- 低端设备【更友好】:编译效率的提升,降低了运行时编译的资源开销,这对硬件资源有限的低端设备尤其有益,有助于缩小不同设备间的体验差距。
3. 具体是怎么优化的?
这次的优化方向主要是:剔除低效代码,优化数据结构与算法。
优化1:剔除无效遍历,拒绝无用功
编译器内部有许多优化阶段(Pass),例如全局值编号(GVN)。官方发现,GVN中一个名为Kill的方法,无论实际情况如何,都会遍历所有节点进行检查,而大部分遍历是徒劳的。通过跳过已知无效的遍历,直接将此阶段的运行时间缩短了约15%,整体性能消耗从【1.023%降至约0.3%】
优化2:数据结构优化,从O(n)到O(1)
在LoadStoreAnalysis阶段,一个关键查找函数FindReferenceInfoOf原本采用线性搜索(O(n))。
优化后,将其数据结构改为以指令ID为索引的映射,实现了常数时间(O(1))查找,并预分配空间避免动态调整。仅此一项,就使该阶段加速34-66%,总编译时间提升【0.5-1.8%】。
优化3:内联检查前置,避免无效计算
编译器内联函数时,原本流程是先进行大量计算,最后才做资格检查。现在将如指令数检查等启发式规则前置,在计算前就过滤掉明显不符合条件的情况,避免了大量无效计算,带来了约【2%】的提升
优化4:适配现代使用模式,重构历史负担
代码库中遗留了一个为处理大型集合而优化的自定义HashSet。然而,当前的实际使用场景是创建大量小型、短生命周期的集合。通过调整实现以适应“小而短”的用法,减少了创建和销毁开销,编译时间提升【1.3-2%】,内存占用反而下降
4. 优化的同时带来了什么副作用?
在实际调整中,官方在落地这些调整时遇到了许多问题,因为当你修改了问题A后,往往容易引入更多的问题BCD。
但在最后结果中,实际上没有带来任何的【副作用】。在这里官方分享了其中几个比较关键问题,如:内存回归问题、历史遗留债务、方案过于复杂。
4.1 该重构就重构
在性能优化中,非常容易出现内存回归问题,即:提升过程中增加了内存占用峰值。
具体到这里的场景是:在优化“输出写入”阶段时,团队通过缓存计算值来加速(原本预计提升 1.3-2.8%),但自动化测试时发现,额外的缓存数据结构导致了内存使用量的显著增加
针对这个问题,官方并没有妥协,而是选择重构该阶段的底层逻辑,移除了冗余数据结构,最终不仅解决了内存问题,还获得了额外的速度提升:【0.5-1.8%】。
4.2 利用先进工具精准定位
团队广泛使用 pprof 进行深度分析,如生成 Flame Graph和 Bottom-up 视图,精准定位那些隐藏在代码深处的、“隐形”的性能开销,如频繁的对象拷贝。
4.3 “快速迭代”验证策略
为了高效验证优化思路,团队采用原型开发方式(Prototype),在典型应用(First-party apps, Android OS)上快速验证想法,确认收益后再进行完整的工程实现,节省了宝贵的开发时间。
总结
在复杂的历史代码中抽丝剥茧,用精准的手术刀取代粗暴的重锤
这是我对Android官方这次编译速度优化的评价,他们展示了教科书般的【既要又要】的性能优化最佳实践:我不仅要提升编译速度,同时要保持其他方面的体验与性能:如内存占用、稳定性等指标。