本文要点讨论全部模块化后带来的依靠解析瓶颈,首要包含对头文件增量编译剖析等内容。
优化计划根据 Swift Toolchain 源码,本文不再讨论 Toolchain 相关基本概念及装备流程等,仅聚焦计划本身。
布景
随着混编落地的事务场景越来越多,越来越大,开发中出现的性能痛点开端闪现,问题很明显集中在被 Swift 环境所依靠的 OC 仓的头文件改动上。因而基建架构把要点放在接口层依靠的性能剖析上,力求处理性能瓶颈。
抖音根底技能团队凭借自界说 Toolchain 才能,经过自界说编译参数,裁剪 Clang Header 指定内容,终究完结编译提速 60% 。
本计划已于 2022 年 11 月底上线,在抖音稳定运转近 5 个月。下面就让咱们一同回顾下整个计划从提出到落地的全进程。
初步剖析
在混编场景下,若要确保 OC 与 Swift 间尽或许充分地互操作,则模块化的启用无法仅用在 Swift 编译上下文中——Swift编译导出的 Clang Header,在工程中以$(project_name)-Swift.h
办法出现,将其需求被 re-export 引证的 OC 依靠项,以模块的办法导出,这就意味着若 OC 编译不启用模块化,则无法正确运用 Swift 供给的头文件。
如图,二者不可兼得,Objc Pod D 为了能够解析句子@import A;
而引进A.modulemap
,则其与 A 的互操作不或许再根据文本导入的逻辑,而全面转向模块化。
关于抖音而言,巨型 OC 项意图许多头文件传递依靠的前史包袱,使得在 OC 编译中引进模块化是一场灾祸。模块化环境下,缓存体系抉择是否要射中 .o 缓存的耗时,比文本环境下重新编译耗时还要长;增量编译时,也会导致广泛的模块重编,改动一个头文件,就要等候数分钟。
传递依靠管理是一项长时间工程,但编译优化等不了那么久,咱们需求一个能够快速处理的计划。
优化效果
在介绍计划之前,先上结论。
在抖音工程中选取代码量最大的 OC&Swift 混编库房进行测验:
- OC 增量编译:选取被 Swift 依靠的 OC 接口层头文件进行改动,编译耗时下降 60%
- Swift 增量编译:选取被 OC 依靠的 Swift public class 进行改动,编译耗时相近,无变化
- 全量编译:清除本地编译缓存进行 clean build,编译耗时下降 17%
可见该计划对编译速度的巨大提高。接下来就让咱们回顾一下整个计划从预研到上线的进程。
计划原理
处理问题的关键在于下降将 OC 头文件预编译耗时,这里有两个思路:
- 长时间:模块解析的耗时本源在于传递依靠,模块的特性导致不同模块内包含的头文件的传递依靠会将模块增量重编的影响范围扩展到很大。事务库在现有工程架构体系下现已严格操控了接口层传递依靠,因而长时间计划会逐步推动管理根底库的传递依靠问题。
- 短期:将 OC 头文件预编译转回文本导入,即裁剪
-fmodule-map-files
注入,但仍然保存对 OC 调用 Swift 代码的支撑
Swift会将本身接口层(即 public/open )声明运用到的 C/OC 模块,在xxx-Swift.h
中以@import aaa
办法给出,这就要求 OC 侧运用该头文件时也需求将这些模块对 OC 侧可见,咱们想要达到意图,就需求对这些声明进行裁剪。这需求自界说东西链的支撑。
本次优化计划效果测验针对的是短期计划。
经过修正编译器,对 Swift 编译生成的 Clang Header Interface 进行裁剪,删除掉体系库以外的 @import,而 OC 侧引证该头文件的当地手动补全依靠。即以暂时献身接口self-contained为代价,使OC侧不必再关心模块相关的要素。为支撑更细粒度的操控,经过向编译器注入编译参数,以针对不同组件操控此功用的启用,以及完结更详细的裁剪内容。
而关于-fmodule-map-files
的裁剪相对容易,只需修正OTHER_CFLAGS
即可封闭-fmodule-map-files
的注入。
预研
计划拆解
咱们先来对整个计划做一个使命拆解,能够剖析出各部分的依靠关系,节省预研阶段的耗时。
一个东西链相关的落地计划,有必要确保其稳定性,因而一定是能够经过一种简略的办法进行外部操控开关的。
从发版视点讲,东西链发版并不像事务代码,和存放在开发库房的装备一样能够灵敏发版,因而应尽或许确保东西链代码的稳定,非必要不修正。
根据这两个准则,咱们能够拆解为:
1.剖析swiftc
的参数解析机制,在编译时的参数列表中拼接新的自界说参数以操控裁剪才能。swiftc
是实践的前端swift-frontend
的一个入口,下面会详细说到,向 swiftc 注入的参数列表,在各swift-frontend
子使命中并不总是以相同的全集出现,效果机制需求进一步剖析。
2.根据细粒度操控的考量,参数选择传入一个装备文件,包含一个白名单,来确认哪些@import Module
是能够留下的。咱们也有考虑过黑名单,但实践工程的依靠状况是杂乱的,不论是 Cocoapods 仍是 seer ,都仅能描述工程层面的依靠状况,而不能确保实践编译时的依靠状况,难以构建一个全面的事务黑名单。而体系库白名单是相对固定的,并不需求经常维护。
3.寻觅生成 -Swift.h 的详细函数,以及写入@import Module;
的逻辑以进行裁剪。
4.在写入逻辑处加载白名单文件并进行过滤。
5.经过本地验证,完结无感知下发 Toolchain 的验证,打出测验 Toolchain 。
6.灰度验证。
7.合码发版上线。
快速验证
想要验证方向是否正确,一起给予饱尝编译耗时困扰的事务同学以决心,需求先找到最关键的点快速验证。
因而咱们决定先直接全体关掉所有 -Swift.h 的@import Module;
生成逻辑。此刻咱们对全体 Swift 源码的认知还较为含糊,但咱们只需求去寻觅相似<< "@import"
或其他写文件的逻辑再去挑选即可,所幸这一进程没有花费太久。
咱们很快找到了这块逻辑,并直接将out << "@import " << Name.str() << ";\n";
注释掉,打包验证成功,出具了本文开头的数据陈述,给事务同学吃下一颗定心丸。
接下来,咱们就能够稳健地墨守成规地去履行其他使命了。
开发、调试
swift-frontend 参数解析流程
所以咱们将目光转向了其他在前端层级应用的原生参数,并参阅它们的写法。很快咱们将目光锁定在module-cache-path
,这是一个 Swift 前端编译必需的参数,指定模块缓存方位,且后面传入一个途径,完全符合咱们的要求。
根据对该参数的剖析,可得 -frontend 阶段的参数解析流程,详细调研进程不再打开,直接简略过下流程。
简略流程如上图,下面详细过下修正参数解析流程的代码方位。
界说
此处运用了一种非常相似 python 的,LLVM 推出的TableGen(llvm.org/docs/TableG… flag ,咱们需求的是
- FrontendOption 前端参数,具有这个flag才会进入前端参数解析流程,而 Clang Header 生成的进程就产生在前端流程中
- ArgumentIsPath 参数为途径,奉告编译器该参数后携带途径字符串作为参数
仿照这种办法的自界说参数:
第二个 EQ 界说其实是一种 Alias,界说了能够运用” flag=arg “这种办法来进行传参,没有其他额外效果。
经过tablegen
东西,把 Options.td 的内容生成为 Options.inc ,如下图
结合 Swift 源码中 Options.h 的 OPTION 界说,引进并供给应 cpp 代码运用
解析
解析进程产生在 CompilerInvOCation 的参数解析流程中
在 ArgsToFrontendOptionsConverter 办法中,从参数列表读取需求的信息,赋值到 Opts 傍边
Opts 是一个 FrontOptions 类型的实例,咱们需求在这里界说一个字符串以存储咱们需求的参数
Opts 会在整个前端流程中流转,为各环节供给必要参数。
Clang Header 生成流程
调用进程的流程图如下,PrintAsClang 是一个相对独立的模块,咱们改动只需求关注这两个标红环节即可。
添加入参界说
在原办法界说上加入两个传参,分别是咱们传入的白名单文件途径,以及诊断信息,诊断信息后面会说到,用于提示一些自界说过错。
这里也是相同,添加两个参数界说。
白名单解析
printAsClangHeader 这里是咱们的首要修正之一,在这个 function_ref 傍边,咱们对 allow list path 指向的文件进行了内容解析,得到白名单指定的模块名称,以参数办法传递给下一个环节。
writeImports 办法在原有根底上添加一个 function_ref,能够理解为lambda
表达式,便是咱们刚刚做的白名单解析的进程。
在详细写入@import Module;
处进行白名单挑选,在白名单内部的答应写入,否则跳过。
自界说诊断信息
DiagnosticsClangImporter.def 中加入两个自界说条目,error 用于提示解析过错,note 仅提示白名单为空,为空是答应的操作,此刻退化为默许逻辑。
前面咱们在办法界说中传入了 Diags 实例,想要提示信息,只需简略调用即可,note 只会输出到日志,error 则会打断编译流程。
验证、上线
可运用云构建机器打出测验 Toolchain,下载至本地,集成到 Xcode 中在抖音验证即可。
将自界说参数加入到指定混编组件的编译参数傍边,即可成功构建。
后记
Swift 东西链定制是一个具有无限或许的方向,包含编译优化这类功率提高的工作等等,都能够在底层进行传统意义上的架构层所难以进行的深度优化,后续针对这块可做的事还有许多,相信有更多的经验能够分享给到大家。