19、编译性能优化:并行编译、分布式编译(distcc)、编译时间分析、构建图优化

说到AOSP编译,我猜你肯定经历过那种“改一行代码,等半小时编译”的绝望。嗯,我早期做系统开发时,最怕的就是下午提交代码,然后盯着屏幕看进度条慢慢爬。后来我花了大量时间研究编译性能优化,今天就把这些经验掰开揉碎讲给你听。

19.1 并行编译:让CPU火力全开

说白了,并行编译就是让CPU的每个核心都干活,别闲着。AOSP默认的make命令是单线程的,你得手动告诉它:“嘿,用上所有核心!”

核心命令:

make -j$(nproc)   # 使用所有CPU核心
make -j16         # 指定16个并行任务

我个人习惯用 -j$(nproc),这样能自动适配机器配置。但要注意,不是越大越好。我曾经在一台32核的服务器上试过 -j64,结果内存爆了,编译直接挂掉。

经验之谈:并行数建议设置为CPU核心数的1.5倍到2倍。比如8核CPU,用 -j12-j16 比较稳妥。这样既能充分利用CPU,又不会把内存吃光。

19.2 分布式编译(distcc):让多台机器一起干活

单机性能再强也有上限。当你需要编译整个AOSP时,一台机器可能要跑一两个小时。这时候,分布式编译就派上用场了。

distcc的原理很简单:把C/C++的编译任务分发给网络中的多台机器,每台机器编译一部分,最后汇总。我曾在团队里搭过一套distcc集群,把编译时间从90分钟压缩到了25分钟。

19.2.1 搭建distcc环境

# 在每台机器上安装distcc
sudo apt-get install distcc distcc-pump

# 配置distcc主机列表(在编译机上)
export DISTCC_HOSTS="192.168.1.10 192.168.1.11 192.168.1.12"

# 启动distcc服务
sudo systemctl start distccd

注意:所有参与编译的机器必须使用相同的编译器版本和系统库,否则会出现链接错误。我曾经因为一台机器上的gcc版本不同,排查了整整一个下午。

19.2.2 在AOSP中使用distcc

# 设置distcc作为编译器
export CC="distcc gcc"
export CXX="distcc g++"

# 编译时指定并行数
make -j40  # 假设你有5台4核机器,总共20核,用-j40

你想想看,如果每台机器都是8核,5台机器就是40核。用 -j80 甚至更高,编译速度能翻好几倍。但要注意网络延迟,千兆网络是基本要求。

19.3 编译时间分析:找到瓶颈在哪

优化之前,先得知道时间花在哪。AOSP提供了几个好用的工具。

19.3.1 使用ninja的统计功能

# 查看编译耗时统计
ninja -C out/target/product/xxx -j1 -v 2>&1 | tee build.log

# 分析最耗时的模块
grep "elapsed" build.log | sort -k2 -rn | head -20

我记得有一次,我发现一个叫 libart 的模块占了总编译时间的30%。后来一查,是因为它的依赖关系没配好,每次都要重新编译。调整依赖后,时间直接砍半。

19.3.2 使用编译时间分析工具

# 生成编译时间报告
make -j$(nproc) 2>&1 | tee build.log
./build/soong/scripts/build_time.py build.log > report.html

这个报告会告诉你每个模块的编译时间、依赖关系、以及哪些模块可以并行编译。说白了,就是一张“时间账单”,让你知道钱花在哪了。

19.4 构建图优化:让依赖关系更合理

AOSP的构建系统基于ninja,而ninja的核心就是构建图(build graph)。构建图决定了哪些任务可以并行,哪些必须串行。

我刚开始接触AOSP时,总觉得构建图这东西太抽象。后来有一次,我手动画了一张依赖图,才发现问题所在:一个底层库被上百个模块依赖,但它本身编译很慢,导致所有依赖它的模块都在等它。

优化策略:

  • 减少不必要的依赖:检查Android.bp文件,去掉那些“顺手加上”的依赖
  • 拆分大模块:把一个大模块拆成多个小模块,让它们可以并行编译
  • 使用预编译:对于很少变动的库,直接使用预编译版本

19.4.1 可视化构建图

# 生成构建图
ninja -C out/target/product/xxx -t graph > build_graph.dot

# 转换为图片(需要安装graphviz)
dot -Tpng build_graph.dot -o build_graph.png

看着那张图,你就能直观地看到哪些模块是“瓶颈节点”。我通常会把那些入度(被依赖数)特别高的节点标红,重点优化。

19.5 实战:一个完整的优化案例

让我给你讲一个真实的案例。去年我优化一个车载系统的AOSP编译,初始编译时间是2小时10分钟。

优化步骤 操作 编译时间 提升
1 启用并行编译(-j16) 1小时50分 15%
2 搭建distcc集群(4台机器) 45分钟 59%
3 优化构建图(拆分大模块) 32分钟 29%
4 使用ccache缓存 28分钟 12%

最终,编译时间从2小时10分钟降到了28分钟。你想想看,这意味着一台机器一天能编译17次,而不是原来的7次。对于频繁迭代的项目来说,这简直是质变。

避坑指南:我曾经在优化构建图时,把一个模块拆得太细,结果依赖关系变得极其复杂,反而增加了编译时间。后来我总结了一个原则:模块粒度以“编译时间不超过5分钟”为准,太小的模块没必要拆。

19.6 知识体系总览

下面这张图总结了编译性能优化的核心知识体系,你可以把它当作一个检查清单:

编译性能优化 并行编译 -j$(nproc) CPU核心数×1.5~2 避免内存溢出 分布式编译 distcc集群 统一编译器版本 千兆网络要求 编译时间分析 ninja统计 build_time.py 定位瓶颈模块 构建图优化 减少依赖 拆分大模块 预编译稳定库 目标:从2小时到28分钟,提升编译效率4倍以上

这张图把四个优化方向串起来了。你可以先从并行编译入手,这是最立竿见影的。如果还不够,再考虑分布式编译和构建图优化。编译时间分析则贯穿始终,帮你找到下一步该优化什么。

嗯,编译性能优化这件事,说白了就是“让机器多干活,让人少等待”。每次看到编译时间缩短,我都觉得特别有成就感。希望这些经验能帮你少走一些弯路。


公众号:蓝海资料掘金营,微信deep3321