[译] 优化 Swift 的编译时间

本文讲的是[译] 优化 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日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-09-20 00:07:28

[译] 优化 Swift 的编译时间的相关文章

C++编译时间过长解决方案

对于一个中型或者以上项目,编译时间本来就不短,如果在编码过程中,一些问题不注意,将使编译时间更长,下面介绍几点需要注意的地方.   关于<C++ coding Standards>以下几条整改原则: 关于include的原则最多,因为包含头文件相当于将代码复制到本文件来编译,而头文件又经常是用来被别人包含的,所以工程文件多了,每个文件都有include链(包含的文件又include了其他文件),该链条不会止步 于你工程,而会延伸到你所有使用的第3方库里面. 能够去掉的include就去掉 说明

[译] 探索 Swift 4 中新的 String API

本文讲的是[译] 探索 Swift 4 中新的 String API, WWDC 已经结束了(我觉得是自 2014 年来最好的一场 WWDC),同时 Xcode 9 beta 版也发布了,很多开发者已经开始把玩 Swift 4 ,今年的新版本真心不错,这是一个改进版本而不是重构版本(像 Swift 2 和 3),因此大多数代码升级起来会更容易. 其中一个改进是 String 的 API,在 Swift 4 中更易用,也更强大.在过去的 Swift 版本中,String API 经常被提出为一个例

[译] 优化 Facebook 新鲜事,使其为您提供更好的服务

本文讲的是[译] 优化 Facebook 新鲜事,使其为您提供更好的服务, 原文地址:Evolving the Facebook News Feed to Serve You Better 原文作者:Ryan Freitas 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:Lai 校对者:kyrieliu Sean Shao 从去年年底开始,我们就着手探索如何让新鲜事(News Feeds)更加易于阅读.易于交流和易于浏览.可以想象,为一个连接 20

java-开发JAVA程序如何获取系统编译时间?

问题描述 开发JAVA程序如何获取系统编译时间? 开发JAVA程序如何获取系统编译时间?哪位大神知道的给条具体思路啊,在线坐等,谢谢! 解决方案 long start = System.currentTimeMillis();/*中间是你需要测试的代码*/long end = System.currentTimeMillis(); long time = end-start; 解决方案二: 这我猜要改编译器才可以 解决方案三: 这个问题很好解决呀!创建对象就可以了,直接new Date() 不知

[译] 模块化 Swift 中的状态

本文讲的是[译] 模块化 Swift 中的状态, 原文地址:Modelling state in Swift 原文作者:John 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:Deepmissea 校对者:atuooo 模块化 Swift 中的状态 在构建应用或设计系统的时候,最困难的事情之一就是如何建模并处理状态.当应用的某些部分处于我们意料之外的状态时,管理状态的代码也是一个非常常见的 bug 来源. 这周,让我们看一看能更容易处理并响应状态改变

ART世界探险(19) - 优化编译器的编译流程

ART世界探险(19) - 优化编译器的编译流程 前面,我们对于快速编译器的知识有了一点了解,对于CompilerDriver,MIRGraph等都有了初步的印象. 下面,我们回头看一下优化编译器的编译过程.有了前面的基础,后面的学习过程会更顺利一些. 下面我们先看个地图,看看我们将遇到哪些新的对象: OptimizingCompiler::Compile 我们先来看看优化编译的入口点,Compile函数: CompiledMethod OptimizingCompiler::Compile(c

《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #6 使用localmodconfig缩短编译时间

HACK #6 使用localmodconfig缩短编译时间 本节介绍使用make localmodconfig生成精简的.config文件,缩短内核编译时间的方法. 为了能够应对各种各样的环境,发布版的内核包含很多内核模块.但是在某个特定机器,例如,大家自己平时使用的PC上实际用到的模块只是其中的极小一部分.重新构建内核时,对不使用的模块进行编译就会浪费时间.编译后的模块存放在磁盘里,因此也会造成磁盘空间的浪费. 将localmodconfig作为make的目标,就可以生成仅以正在使用的内核模

Nginx/Apache 对图片,css,js等优化,静态页面设置过期时间

图片,CSS,JS,html设置过期时间 不是本域名的重定向到本域名 Nginx 图片,css,js等优化,静态页面设置过期时间 server{ ... location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ { expires 30d;#图片缓存30天 } location ~ .*\.(js|css)?$ { expires 12h;#js css缓存12小时 } ... } 以上是在NGINX.CONF里复制的 我的站静态页面是如下设置的,伪静态也适用,这种方

ios-iOS项目过大,每次更新或clean编译时间过长

问题描述 iOS项目过大,每次更新或clean编译时间过长 iOS项目过大,文件太多,每次更新或clean之后编译时间过长 解决方案 不可避免的问题,不用纠结 解决方案二: 为什么要一直清理工程? 解决方案三: 先精简你的代码,把一些不用的代码,framework等都先删除了,这样clean会快.