规范写法提高swift的编译速度
Posted 想名真难
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了规范写法提高swift的编译速度相关的知识,希望对你有一定的参考价值。
文章将从两方面来介绍如何提高swift项目的编译速度,一是从代码优化上,一是从编译器设置上。
在改善项目的编译速度前,有必要知道到底是哪些函数编译耗时,哪些文件编译耗时.
Robert 一个swift爱好者为我们提供了一个统计函数编译时间的工具https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode,利用该工具能很方便的查出编译耗时的地方。
也可以使用xcode自带的工具查看编译时间
方法1. 在target
-> Build Settings
-> Other Swift Flags
添加编译设置
-Xfrontend -debug-time-function-bodies
查找耗时代码
xcodebuild -workspace yourWorkspaceName.xcworkspace -scheme schemeName clean build 2>&1 |egrep "\\d.\\dms"|sort -nr > times.txt
sort -nr
会按照时间大小排序,当编译完成后,times.txt里可以查看到各个方法编译的时间
然后解决掉前面比较耗时的代码 编译就会相对快了
方法2. 不在Build Settings中添加编译设置
xcodebuild -workspace yourWorkspaceName.xcworkspace -scheme schemeName clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | egrep "\\d.\\dms" | egrep -v "\\b0.0ms" > times.txt
此处增加的
egrep -v "\\b0.0ms"
可以排除掉编译时间为0.0ms的方法-workspace yourWorkspaceName.xcworkspace
在没有workspace 可以省略-scheme schemeName
没有workspace切仅一个target时可省略
代码层面优化
1.尽量避免类型推断,能确定类型的一定要给出具体类型
func test1()
let number = 32
let string = ""
let label = UILabel()
let dict = ["string1":"string","number":10,"label":label] as [String : Any]
var strings: [String] = []
func test2()
let number:Int = 32
let string:String = ""
let label:UILabel = UILabel()
let dict:[String:Any] = ["string1":"string","number":10,"label":label] as [String : Any]
var strings: [String] = [String]()
test1采用了类型推断耗时37.3ms,test2采用了精确的类型定义耗时10.5ms,减少了近三倍多的编译时间。
复杂类型的类型推断更是恐怖, eg:字典
这里引入debugging-slow-swift-compile-times的一个🌰
//50612.1ms
[
"A" : [
["B": [1, 2, 3, 4, 5]],
["C": [ ]],
["D": [ ["A": [ 1 ]]]]
]
]
// 8.8ms
[
"A" : [
["B": [1, 2, 3, 4, 5]] as [String: [Int]],
["C": [ ]] as [String: [Int]],
["D": [ ["A": [ 1 ]] as [String: [Int]]]] as [String : [[String: [Int]]]]
]
]
2.nil类型问题
由于swift存在可选值,因此某些对象的值可能为空,这在代码处理时可能会导致编译很慢
func test3() ->Int
var number1:Int?
var number2:Int?
var number3:Int?
return 10 + (number1 ?? 0) + (number2 ?? 0) + (number3 ?? 0)
func test4() ->Int
var total = 10
var number1:Int?
var number2:Int?
var number3:Int?
if let number1 = number1
total = total + number1
if let number2 = number2
total = total + number2
if let number3 = number3
total = total + number3
return total
test3中number1,2,3可能存在nil,因此在返回时如果为nil,则给了默认值0,结果编译时间为7841.3ms,将近8s,太不可思议。而test4中对于可能为nil的情况下进行了可选值绑定来判断是否为nil,编译时间为1.7ms,编译时间跟test3不在一个量级上面,因此对于可能为nil的情况下,建议采用可选值绑定的方式来判断,避免采用三的处理方式。
还有就是 ?? 运算符, 虽然用起来很爽, 但是真的会导致编译很慢, 建议 将??运算的结果用变量存起来再赋给属性
// 2.6ms ?? 与其它操作符一起用 -- 此处可能会耗时,笔者遇到这里耗时200ms+的情况,修改了后好了些
var optionalInt: Int? = 10
let plus = (optionalInt ?? 0) + 10
// 0.5ms 使用变量将?? 的值存起来 再进行预算
var optionalInt: Int? = 10
var nonnullInt = optionalInt ?? 0
let plus = nonnullInt + 10
//10.8ms 直接将??运算的结果赋给属性 可能会很耗时!!!
let label = UILabel()
let optionalStr : String? = nil
label.text = optionalStr ?? ""
// 0.3ms ??运算的结果用变量存起来再赋给属性
let label = UILabel()
let optionalStr : String? = nil
let displayText = optionalStr ?? ""
label.text = displayText
3.+ +=运算
func test5()
var arrays = [Int]()
let arr1 = [1,2,3]
let arr2 = [3,4,5]
arrays += arr1 + arr2 + [10]
func test6()
var arrays:[Int] = [Int]()
let arr1 = [1,2,3]
let arr2 = [3,4,5]
arrays.append(contentsOf: arr1)
arrays.append(contentsOf: arr2)
arrays.append(contentsOf: [10])
test5采用+ 将数组进行合并耗时140.9ms,而test5采用系统提供的api进行合并耗时2.3ms,因此对于数组合并的情况建议采用test6的形式。
4.复杂表达式计算
func test7(string1:String,string2:String)
let string = string1 + "你好" + string2 + "\\(10)"
func test8(string1:String,string2:String)
var string = string1
string = string + "你好"
string = string + string2
string = string + "\\(10)"
test7表达式虽清晰,但复杂,编译耗时23.4ms,test8将test7的表达式拆成几部分,编译时间1.3ms,表达式越简单,编译时间越短,因此是编写简洁明了的表达式,还是编写对编译器友好的表达式,我们是需要权衡的。
5.函数放在extension中,比不放在extension中编译更耗时,使用闭包也比较耗时。
6.lazy属性
//
private lazy var label: UILabel =
let l = UILabel()
l.font = UIFont.systemFontOfSize(19)
return
()
//
private lazy var labe1: UILabel =
$0.font = UIFont.systemFontOfSize(19)
return $0
(UILabel())
private var label2: UILabel!
self.label2 = UILabel()
self.label2.font = UIFont.systemFontOfSize(19)
之前在解决编译慢时 完全没想到 lazy属性 会有影响,编译时间多大200ms+, 如果仅被编译一次,那就无关痛痒
当一个类使用的地方多的时候,这个类会多次进行编译,假如一个类在10处有使用,则该类会被编译20次😱, 200ms * 20 = 4s, 这样算起来就... 大家可以自己想象
所以把所有的lazy属性都换掉了 , 出了属性初始化, 在集合操作中使用lazy也会导致编译慢
//20.3ms
func testLazyMap()
let intArr = (0..<100).map$0
let lazyMapResult: [String] = intArr.`lazy`.map String($0)
7.5ms
func testDirectMap()
let intArr = (0..<100).map$0
let lazyMapResult: [String] = intArr.map String($0)
lazy 比非lazy相对耗时,在编译慢时时间相差会比较明显
206.6ms
func test_appendLazyMapArray()
let intArr = (0..<100).map$0
var result: [String] = []
result.appendContentsOf(intArr.lazy.map String($0) )
25.9ms
func test_appendMapArray()
let intArr = (0..<100).map$0
var result: [String] = []
result.appendContentsOf(intArr.map String($0) )
直接append 带lazy的数组和不带lazy的数组,不带lazy的方式编译快
7.4ms
func test_appendMapedVar()
let intArr = (0..<100).map$0
var result: [String] = []
let maped = intArr.map String($0)
result.appendContentsOf(maped)
33.0ms
func test_appendLazyMappedVar()
let intArr = (0..<100).map$0
var result: [String] = []
let maped = intArr.lazy.map String($0)
result.appendContentsOf(maped)
带lazy的同样比无lazy的慢, 所以开发过程中 能不用lazy就不用lazy
8.闭包
8.1不要嵌套闭包
Swift推断闭包(方法)需要把内部所有代码都推断完才能推断出闭包的类型, 这时候推断效率会变得奇低, 速度最低时只有正常情况下的1/10, 如:
fileprivate lazy var bindPhoneCell: AccountBindButton =
let cell = AccountBindButton()
cell.rx.controlEvent(.touchUpInside).subscribe [weak self] (_) in
...
.disposed(by: disposeBag)
return cell
()
8.2属性能不用闭包尽量不要用闭包(带闭包的)初始化, 或者类似于:
private lazy var wechatContact = VerticalAlignButton(type: .custom).config
$0.innerSpace = 12
8.3有返回值的闭包比无返回值的闭包需要更长的推断时间(大概1.5到2倍), 但除非闭包非常复杂否则差别不大
9.重载函数
重载的函数会增加3到10倍的推断时间, 比如:
Int(floor(progress * 100)) 和 (progress * 100).floor.int
点名批评一下Snapkit和RxSwift这两个用了大量重载函数的库, 超过100ms的代码一半都是这两个库的闭包方法, 还有WCDB.swift的where语句也难以推断, 简单的像:
Properties.userId == 0 && Properties.resourceId == 1 && Properties.chapterId == 2
就得推断140ms, 服了
二 编译器层面优化编译时间#
1.WHO
简单地说,Whole-Module Optimization(全模块优化,以下简称 WMO),即在编译项目时,将同属于一个 Module(可以理解为一个 Target、一个 Package)的所有源代码都串起来,进行整体的一个分析与优化,区别于 Single-File Optimization(单文件优化,以下简称 SFO),WMO 可以更好的统筹全局,去 inline 函数调用、排除死函数(即写了却从不调用的函数)等等,使编译速度加快。但问题来了,WMO 只是在 Release 模式下成为了默认且推荐的选项,在 Debug 模式下默认依然是 None。
2.利用Uber团队在利用swift3重写客户端中发现的黑科技
Uber 的开发团队偶然发现如果把所有 Model 文件全部合并到一个文件去编译, 那编译时间会从 1min 35s 减少到 17s, 那么我们如果把所有代码文件都合并到一起, 那就可以极大地优化编译速度了。
WHO(Whole-Module-Optimization) 也会把文件合并起来再进行编译, 实际使用时我们发现编译虽然快了, 但对于编译时间的减少还是远没有直接把文件合并到一起那么有效. 主要原因是因为 WHO 除了合并文件之外, 还会在预编译阶段做这些事情: 检测没有被调用的方法和类型, 在预编译期去掉它们,给没有被继承的类, 没有被继承的方法加上 final 标签, 给编译器提供更多信息, 以便这些方法被优化为静态调用或者是内联进去,这些优化会对于程序的效率有很大的提升, 但编译时间会有所增加。
Uber 的团队发现通过增加一个编译宏就可以做到只合并文件, 而不做优化. 进入工程文件设置 -> Build Setting -> Add User-Defined Settings, key 为 SWIFT_WHOLE_MODULE_OPTIMIZATION
, value 设为 YES
, 然后把优化级别设为 None
就可以了.
参考链接: 如何有效提高swift的编译速度 - 简书
以上是关于规范写法提高swift的编译速度的主要内容,如果未能解决你的问题,请参考以下文章
使用 Docker 从源代码编译 TensorFlow 以提高 CPU 速度