[译] 优化 Swift 的编译时间

优化 Swift 的编译时间

在 Swift 全部的特性中,有一件事有时会至关恼人,那就是在用 Swift 编写更大规模的项目时,它通常会编译多久。尽管 Swift 编译器在保证运行时安全方面作的更多,可是它的编译时间要比 Objective-C 编译时间长不少。(因此)我想研究一下,是否咱们能够帮助编译器让他工做的更快。javascript

因此,上周我投身于 Hyper 上的一个较大的 Swift 项目。它大概有 350 个源文件以及 30,000 行的代码。最后我设法将这个项目的平均构建时间减小了 20%。因此我想在我这周的博客上详细的介绍我是怎么作的。java

如今,在咱们开始以前,我只想说我不想这篇文章以任何形式的方式来批判 Swift 或它的团队工做。我知道 Swift 编译器的开发者,包含 Apple 公司和开源社区,都在持续地对编译器速度、功能和稳定性作出重大改进。但愿这篇博文能随着时间的流逝而显得多余,但在那以前,我只是想提供一些我发现能够提高编译速度的实用技巧。git

Step 1: 采集数据

在开始优化工做以前,创建一个能衡量你改进的基准老是好的。我是经过在 Xcode 里,给应用的 target 添加两个简单的脚本做为运行脚本阶段来实现的。github

编译源文件以前,添加下面的脚本:json

echo "$(date +%s)" > "buildtimes.log"复制代码

在最后,添加这个脚本:swift

startime=$(<buildtimes.log)
endtime=$(date +%s)
deltatime=$((endtime-startime))
newline=$'\n'

echo "[Start] $startime$newline[End] $endtime$newline[Delta] $deltatime" > "buildtimes.log"复制代码

如今,这个脚本只会测算编译器编译应用本身的源文件的时间(为了测量出整个引用的编译时间,你可使用 Xcode 的特性来挂载(hook)到 Build StartsBuild Succeeds 上)。因为编译时间很是依赖于编译它的设备,因此我也 git ignored 了 buildtimes.log 文件安全

接下来,我想突出哪些个别代码块耗费了额外的长时间来编译,以便识别瓶颈,这样我就能够修复它。要作到这个,只须要经过向 Xcode 中 Build Setting 里的 Other Swift Flags 传递下面的参数给 Swift 编译器来设置一个临界值:frontend

-Xfrontend -warn-long-function-bodies=500复制代码

使用上面的参数后,在你的项目中,若是有任何函数耗费了超过 500 毫秒的编译时间,你就会获得一个警告。这是我开始设置的临界值(而且随着我对更多瓶颈的修复,这个值在不断的下降)。函数

Step 2: 消除全部的警告

在设置了函数编译时间过长的警告以后,你可能会在项目中开始发现一些。最开始,你会以为编译时间过长的函数是随机的,可是很快模式(patterns)就开始出现了。这里我注意到了两个使 Swift 3.0 编译器编译函数时间过长的常见模式:布局

自定义运算符(特别是带有通用参数的重载)

当 Swift 出现时,对于大多数 iOS 和 macOS 开发者来讲,运算符重载是全新的概念之一,但就像许多新鲜事物同样,咱们很兴奋的使用它们。如今,我不打算在这讨论自定义或重载运算符是好是坏,但它们的确对编译时间有很大影响,尤为是若是使用更加复杂的表达式。

思考下面的运算符,它将两个 IntegerConvertible 类型的数字加起来,构成了自定义的数字类型:

func +<A: IntegerConvertible, B: IntegerConvertible>(lhs: A, rhs: B) -> CustomNumber { return CustomNumber(int: lhs.int + rhs.int) }复制代码

而后咱们用它来让几个数字相加:

func addNumbers() -> CustomNumber {
    return CustomNumber(int: 1) +
           CustomNumber(int: 2) +
           CustomNumber(int: 3) +
           CustomNumber(int: 4) +
           CustomNumber(int: 5)
}复制代码

看上去很简单,可是上面的 addNumbers() 函数会花费很长一段时间来编译(在我 2013 年的 MBP 上超过 300 ms)。对比一下,若是咱们用协议扩展来实现相同逻辑:

extension IntegerConvertible {
    func add<T: IntegerConvertible>(_ number: T) -> CustomNumber {
        return CustomNumber(int: int + number.int)
    }
}

func addNumbers() -> CustomNumber {
    return CustomNumber(int: 1).add(CustomNumber(int: 2))
                               .add(CustomNumber(int: 3))
                               .add(CustomNumber(int: 4))
                               .add(CustomNumber(int: 5))
}复制代码

经过这个改变,咱们的 addNumbers() 函数如今编译时间不到 1 ms这快了 300 倍!

因此,若是你大量的使用了自定义/重载运算符,特别是带有通用参数的(或者若是你使用的第三方库来作这些,好比许多自动布局的库),考虑一下用普通函数、协议扩展或其余的技术来重写吧。

集合字面量

另外一个我发现的编译时间瓶颈是使用集合字面量,特别是编译器须要作不少工做来推断那些字面量的类型。让咱们假设你有一个函数,它要把模型转换成一个相似 JSON 的字典,像这样:

extension User {
    func toJSON() -> [String : Any] 
        return [
            "firstName": firstName,
            "lastName": lastName,
            "age": age,
            "friends": friends.map { $0.toJSON() },
            "coworkers": coworkers.map { $0.toJSON() },
            "favorites": favorites.map { $0.toJSON() },
            "messages": messages.map { $0.toJSON() },
            "notes": notes.map { $0.toJSON() },
            "tasks": tasks.map { $0.toJSON() },
            "imageURLs": imageURLs.map { $0.absoluteString },
            "groups": groups.map { $0.toJSON() }
        ]
    }
}复制代码

上面 toJSON() 函数在个人电脑上大概要 500 ms 的时间来编译。如今让咱们试着逐行重构这个像字典的东西来代替字面量:

extension User {
    func toJSON() -> [String : Any] {
        var json = [String : Any]()
        json["firstName"] = firstName
        json["lastName"] = lastName
        json["age"] = age
        json["friends"] = friends.map { $0.toJSON() }
        json["coworkers"] = coworkers.map { $0.toJSON() }
        json["favorites"] = favorites.map { $0.toJSON() }
        json["messages"] = messages.map { $0.toJSON() }
        json["notes"] = notes.map { $0.toJSON() }
        json["tasks"] = tasks.map { $0.toJSON() }
        json["imageURLs"] = imageURLs.map { $0.absoluteString }
        json["groups"] = groups.map { $0.toJSON() }
        return json
    }
}复制代码

它如今编译时间大概在 5 ms 左右,提升了 100 倍!

Step 3: 结论

上面的两个例子很是清晰的说明了 Swift 编译器的一些新特性,好比类型推演和重载,都是付出了时间开销。若是咱们仔细思考一下,也很符合逻辑。因为编译器不得不作更多的工做来执行推演,因此花费了更多的时间。可是咱们也看到了,若是咱们稍微调整一下咱们的代码,帮助编译器更简单的解决表达式,咱们就能够很大程度的加快编译时间。

如今,我不是说你要一直让编译时间来决定你写代码的方式。有时可让它作更多的工做,让你的代码更加清晰而且容易理解。可是在大型的项目中,每一个函数要用 300-500 ms 范围(或更多)的时间来编译的编码技术可能很快就会成为一个问题。个人建议是对你的编译时间保持监控,使用上面的编译标记设置一个合理的临界值,并在发现问题的时候解决问题。

我确信上面的例子确定没有涵盖全部潜在的编译时间改进的方法,全部我很愿意听到你的意见。若是你有任何有用的改进大型 Swift 项目编译时间的其余的技术,你能够写在 Medium 上回复,或者在 Twitter @johnsundell 上联系我。

感谢阅读!🚀

相关文章
相关标签/搜索