写写代码,聊聊生活

0%

Swift 踩坑记录-编译

背景

最近做了个新项目,混编方案,Swift 为主力语言,一开始体验还不错,开发效率挺快,但是随着代码量增多,编译时间越来长,全量编译要400s左右,改动全局公共文件,增量编译要200s左右

排查

起初怀疑混编导致编译慢,网上查了也确实有人遇到类似情况,但是对于新项目来说,也不至于慢成这样,于是新建混编 demo 进行测试,自动生成 Swift 和 OC 类,并设置依赖关系,发现混编对编译时间影响很小,所以排除混编引起的
利用 Xcode 编译设置找到耗时较高的地方,在 Target -> Build Settings -> Swift Compiler 的 Other Swift Flags 中添加了如下配置:

1
2
-Xfrontend -warn-long-function-bodies=100 //编译时间超过100ms的方法会有警告
-Xfrontend -warn-long-expression-type-checking=100 //编译时间超过100ms的表达式会有警告

配置后,对比发现,只要在使用了 == 运算符的地方,编译时间高得惊人:

一个简单方法,居然要6536ms,而且这种警告很多,简直太吓人
,为啥 == 运算符会这么慢,而其他运算符却是正常的?这不合理,第一感觉是有外部因素影响导致的,在 Swift 中,运算符本质就是一个 static 函数,而且是可以重载的,其函数原型为:

1
public static func == (lhs: Type, rhs: Type) -> Bool

由于是 static 函数,编译时,在每个使用了运算符的地方,编译器根据左值和右值的类型在重载函数列表里找到正确的函数调用,因此,重载量和重载后运算符的使用量跟编译时间成正比的,重载越多,使用重载函数的地方越多,查找就越耗时,编译时间也就越长
在项目中搜索,在 protocol buffer 模板里发现了大量重载 == 运算符的结构体,重载了1200多次
为了进一步验证该想法,于是又造了个 demo,用脚本自动生成大量重载 == 运算符的类,使用 == 的地方立马有警告了:

不过这里耗时不算高,主要是因为 demo 中 == 运算符使用不多
至此,根本问题终于找到,接下来就是如何解决了

解决

我们项目使用 protocol buffer 序列化方案,而且没有用 OC 桥接,模板代码是纯 Swift 的,使用的是苹果官方写的构建工具 https://github.com/apple/swift-protobuf/tree/main/Sources/protoc-gen-swift
,生成很多都是冗余的代码,像重载 == 运算符根本用不到,也完全可以用方法代替,于是决定修改工具源码,把生成重载 == 运算符代码去掉

  • 找到 MessageGenerator.swift 文件,该文件里的代码主要用来生成模板代码的,其中生成运算符重载的代码是这个函数:
1
private func generateMessageEquality(printer p: inout CodePrinter)
  • 找到函数调用,把调用注释掉
  • 重新编译,生成新的可执行文件
  • 把打包机器上的构建工具替换掉

把 pb 模板重新打包生成新的代码,新代码里已经没有重载 == 运算符了

效果

全量编译时间由400s变成200s,提升1倍左右,改动全局公共模块,增量编译时间由200s变成30s,提升6倍左右,对于正常编码,原来改动一下小范围依赖的文件,编译都要100s左右,现在基本是秒编译了,编码体验大大提升。

一些小想法

我们使用 protocol buffer 序列化方案时,不管 Swift 还是其他语言,官方的构建工具会生成很多冗余代码,其实这些是通过修改其源码,达到定制优化的目的,给模板瘦身,还可以添加自己的特性代码,做一些扩展

工具

BuildTimeAnalyzer (分析编译时间)