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 知识体系总览
下面这张图总结了编译性能优化的核心知识体系,你可以把它当作一个检查清单:
这张图把四个优化方向串起来了。你可以先从并行编译入手,这是最立竿见影的。如果还不够,再考虑分布式编译和构建图优化。编译时间分析则贯穿始终,帮你找到下一步该优化什么。
嗯,编译性能优化这件事,说白了就是“让机器多干活,让人少等待”。每次看到编译时间缩短,我都觉得特别有成就感。希望这些经验能帮你少走一些弯路。
公众号:蓝海资料掘金营,微信deep3321