规范写法提高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工程编译越来越慢,原来... - 简书

Swift 推断速度优化总结 - 简书

以上是关于规范写法提高swift的编译速度的主要内容,如果未能解决你的问题,请参考以下文章

程序员提高代码编译速度,都怎么做的?

使用 Docker 从源代码编译 TensorFlow 以提高 CPU 速度

嵌入式开发 | 提高单片机代码编译速度的几种方法

嵌入式开发 | 提高单片机代码编译速度的几种方法

Swift新async/await并发中利用Task防止指定代码片段执行的数据竞争(Data Race)问题

Swift新async/await并发中利用Task防止指定代码片段执行的数据竞争(Data Race)问题