[译] 优化 Swift 的编译时间

简介: 本文讲的是[译] 优化 Swift 的编译时间,在 Swift 所有的特性中,有一件事有时会相当恼人,那就是在用 Swift 编写更大规模的项目时,它一般会编译多久。尽管 Swift 编译器在保证运行时安全方面做的更多,但是它的编译时间要比 Objective-C 编译时间长很多。
本文讲的是[译] 优化 Swift 的编译时间,

优化 Swift 的编译时间

在 Swift 所有的特性中,有一件事有时会相当恼人,那就是在用 Swift 编写更大规模的项目时,它一般会编译多久。尽管 Swift 编译器在保证运行时安全方面做的更多,但是它的编译时间要比 Objective-C 编译时间长很多。(所以)我想研究一下,是否我们可以帮助编译器让他工作的更快。

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

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

Step 1: 采集数据

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

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

echo "$(date +%s)" > "buildtimes.log"

在最后,添加这个脚本:

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 Starts 和 Build Succeeds 上)。由于编译时间非常依赖于编译它的设备,所以我也 git ignored 了 buildtimes.log 文件

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

-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 范围(或更多)的时间来编译的编码技术可能很快就会成为一个问题。我的建议是对你的编译时间保持监控,使用上面的编译标记设置一个合理的临界值,并在发现问题的时候解决问题。





原文发布时间为:2017年4月01日

本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。
目录
相关文章
|
8月前
|
安全 编译器 Swift
【Swift开发专栏】Swift的编译优化与构建配置
【4月更文挑战第30天】Swift编译优化与构建配置对开发效率和应用性能至关重要。编译优化包括不同级别的优化、函数内联、泛型特化、尾递归优化、死代码消除和链接时优化。在Xcode的&quot;Build Settings&quot;中可调整相关标志。构建配置涉及Debug与Release模式、自定义配置、条件编译、构建设置和脚本。开发时,应适时测试、选择适当优化级别、避免过度优化,并利用条件编译区分不同版本的代码。有效管理构建设置可提升开发质量和性能。
130 0
|
Swift iOS开发 MacOS
Swift-进阶 01:Swift源码编译
Swift-进阶 01:Swift源码编译
538 0
Swift-进阶 01:Swift源码编译
关于 Swift 编译时性能优化的一些思考
本文讲的是关于 Swift 编译时性能优化的一些思考,一行之前很简洁的代码,现在却出现了新的问题——它是否应该重构为9行代码来达到更快的编译速度? (nil coalescing 运算符就是一个例子)孰轻孰重?简洁的代码还是对编译器友好的代码?
1388 0
|
程序员 Android开发 iOS开发
iOS Swift _Nullable 与 Android 注解帮助编译时检查 - 两家好像步调开始一致一段时间了
iOS Swift _Nullable 与 Android 注解帮助编译时检查 - 两家好像步调开始一致一段时间了 太阳火神的美丽人生 (http://blog.csdn.net/opengl_es) 本文遵循“署名-非商业用途-保持一致”创作公用协议 转载请保留此句:太阳火神的美丽人生 -  本博客专注于 敏捷开发及移动和物联设备研究:iOS、Android、HTML5、Arduino、pcDuino,否则,出自本博客的文章拒绝转载或再转载,谢谢合作。
1616 0
|
Web App开发 Ubuntu
《Swift入门》ubuntu下编译运行Swift开发的Web后端示例
这里只是演示如何在ubuntu下编译运行Swift开发的Web后端项目。 项目代码来自Bluemix上提供的示例代码,如果你有账号,可以去自己的空间下载,没有的话,可以通过下面的地址下载: http://download.
873 0
|
Swift
Swift Beta6 编译之前版本出错
安装Xcode6 Beta6之后,编译之前的项目可能会出问题,比如出现__TFSs26_forceBridgeFromObjectiveCU__FTPSs9AnyObject_MQ__Q_的问题,如下图: 遇到这个问题只要把Derived Data清除掉,重新编译就可以了: 这样一来__TFSs26_forceBridgeFromObjectiveCU__FTPSs9AnyObject_MQ__Q_的问题就解决了。
925 0
|
6月前
|
Unix 调度 Swift
苹果iOS新手开发之Swift 中获取时间戳有哪些方式?
在Swift中获取时间戳有四种常见方式:1) 使用`Date`对象获取秒级或毫秒级时间戳;2) 通过`CFAbsoluteTimeGetCurrent`获取Core Foundation的秒数,需转换为Unix时间戳;3) 使用`DispatchTime.now()`获取纳秒级精度的调度时间点;4) `ProcessInfo`提供设备启动后的秒数,不表示绝对时间。不同方法适用于不同的精度和场景需求。
210 3
|
2月前
|
安全 Swift iOS开发
Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法
本文深入探讨了 Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法。Swift 以其简洁、高效和类型安全的特点,结合 UIKit 丰富的组件和功能,为开发者提供了强大的工具。文章从 Swift 的语法优势、类型安全、编程模型以及与 UIKit 的集成,到 UIKit 的主要组件和功能,再到构建界面的实践技巧和实际案例分析,全面介绍了如何利用这些技术创建高质量的用户界面。
46 2
|
2月前
|
Swift iOS开发 UED
如何使用Swift和UIKit在iOS应用中实现自定义按钮动画
本文通过一个具体案例,介绍如何使用Swift和UIKit在iOS应用中实现自定义按钮动画。当用户点击按钮时,按钮将从圆形变为椭圆形,颜色从蓝色渐变到绿色;释放按钮时,动画以相反方式恢复。通过UIView的动画方法和弹簧动画效果,实现平滑自然的过渡。
81 1