我们有一个梦想,让每一名研发工程师拥有一台“超级”计算机。
作者:字节跳动终端技术——孙雄
大型工程的效率瓶颈
近年来,基于Devops流水线的研发流程,逐渐成为软件研发的行业标准。流水线的运行效率,决定了团队的研发效能。对大型项目来说,编译构建往往是流水线中耗时占比的大头。有些工程的编译时长超过30分钟,甚至达到几个小时。这样的性能,是非常糟糕的。
字节iOS大型项目的构建时长,大多控制在5分钟以内。这主要得益于内部的编译加速解决方案,它集分布式编译和分布式缓存为一体,本文将详细介绍它的工作原理。不过在这之前,我们先来分析一下大型项目的编译瓶颈和解决思路。
先说结论,机器性能不足和重复作业,是影响工程编译效率的两个最大因素,对此,可以采取分布式编译+编译缓存的方式,提升整体的性能。
分布式编译
工程的编译,往往可以拆解为可并行的编译子任务。以C系列语言(C
, C++
, ObjC
)为例,项目中往往存在上千甚至上万的源代码文件(以 .c
, .cc
或 .m
作为扩展名的文件),每个编译子任务将源代码文件编译为目标文件(以 .o
作为扩展名的文件),再整体链接成最终的可执行文件。
这些编译子任务可以并行执行,如下图所示:
CPU的数量,决定了编译的并行度上限。个人电脑(PC)的CPU核心数通常在4~12之间,专用服务器可以达到24~96,但对于动辄上万文件的大型工程,CPU的数量还是显得不足。这时候,利用分布式编译的技术,可以得到一台“超级计算机”。
编译缓存
大型工程全量编译,需要处理几千甚至几万个编译子任务。但大多数子任务,之前已经编译过,如果我们能通过某种方式,直接获取编译产物,就可以大大节省时间。
建立一个中央仓库,存储编译子任务的产物,这些产物可以通过“任务摘要”来索引。这样每次遇到一个新任务,我们首先向中央仓库查询摘要,如果查询成功,直接下载编译产物,就省去了重复编译的动作。
上面提到的分布式编译和编译缓存,是提升大型项目编译效率的两大法宝,本文主要介绍字节跳动的分布式编译解决方案。
“超级”计算机
借助云计算,我们可以以组装的方式,得到一台“超级”计算机,如下图所示:
这台“超级”计算机,由一台中心节点和若干台工作节点组成。中心节点负责生成和调度编译子任务,依照它们的执行顺序,将任务发送给空闲的工作节点来执行。这样整个系统的并行处理能力,取决于所有工作节点的CPU之和,性能比单机高出数倍,甚至数十倍。
像这样把任务分发给工作节点的方案,又称为分布式编译。分布式编译并不是新鲜的概念,2008年开源的distcc工具就提供了分布式编译的解决方案。Google在2017年提出的Remote Execution API,又从协议的角度规范了分布式编译和编译缓存的实现方式。
我们先看一下分布式编译的核心思路。
核心思路
核心思路很简单,本地计算出编译命令需要读的文件,把文件列表和编译命令,发给远端机器,执行编译命令。编译结束后,再请求拉取编译产物。
其中,如何找到所需文件是关键。
背景知识——预处理
在介绍我们的做法之前,需要先补充一些编译原理相关的背景知识。
待编译的源文件,可以通过#include xx.h
和 #import xx.h
的方式,声明对某头文件的依赖。<