为什么Swift的编译时间如此缓慢?

221

我正在使用 Xcode 6 Beta 6。

这是一件困扰我已经有一段时间的事情,但现在已经到了几乎无法使用的程度。

我的项目开始拥有 65 个 Swift 文件和一些桥接的 Objective-C 文件(它们并不是问题的原因)。

似乎对任何一个 Swift 文件进行轻微修改(例如在应用程序中几乎不使用的类中添加一个简单的空格)都会导致为指定目标重新编译整个 Swift 文件。

经过深入调查,我发现几乎占用了 100% 编译器时间的是 CompileSwift 阶段,Xcode 在您的目标的所有 Swift 文件上运行 swiftc 命令。

我进行了进一步调查,如果我只保留带有默认控制器的应用程序委托,则编译非常快。但随着我添加越来越多的项目文件,编译时间开始变得非常缓慢。

现在仅有 65 个源文件,每次编译需要大约 8/10 秒钟。根本不够“swift”。

除了 这个 之外,我没有看到任何讨论此问题的帖子,但那是旧版本的 Xcode 6。所以我想知道是否只有我处于这种情况。

更新

我检查了一些在GitHub上的 Swift 项目,例如AlamofireEulerCryptoSwift,但它们中没有一个拥有足够的 Swift 文件来进行比较。我找到的唯一一个开始变得“不错”的项目是SwiftHN,即使它只有十几个源文件,我仍然能够验证相同的情况,一个简单的空格整个项目都需要重新编译,这开始需要一点时间(2/3 秒钟)。

与 Objective-C 代码相比,分析器和编译都快得惊人,这真的感觉像是 Swift 永远无法处理大型项目,但请告诉我我错了。

使用 Xcode 6 Beta 7 更新

仍然没有任何改进。这开始变得荒谬了。由于 Swift 中缺少 #import,我真的看不出 Apple 如何能够进行优化。

使用Xcode 6.3和Swift 1.2更新

苹果公司增加了增量编译(以及许多其他编译器优化)。您需要将代码迁移到Swift 1.2才能看到这些好处,但是苹果在Xcode 6.3中添加了一个工具来帮助您完成此操作:

Enter image description here

然而

不要像我一样太快高兴。他们用于使构建增量的图形求解器尚未优化得很好。

首先,它不会查看函数签名更改,因此,如果您在一个方法的块中添加空格,则所有依赖于该类的文件都将重新编译。

其次,它似乎基于重新编译的文件创建树,即使更改不影响它们。例如,如果您将这三个类移动到不同的文件中:

class FileA: NSObject {
    var foo:String?
}
class FileB: NSObject {
    var bar:FileA?
}
class FileC: NSObject {
    var baz:FileB?
}
如果你修改了FileA,编译器会显然标记FileA需要重新编译。它还会重新编译FileB(根据对FileA的更改是可以接受的),但因为FileB被重新编译了,所以它也会重新编译FileC,这很糟糕,因为FileC从未在此处使用FileA

因此,我希望他们能改进这个依赖树求解器... 我用这个示例代码打开了一个radar

更新:使用Xcode 7 beta 5和Swift 2.0

昨天,苹果发布了beta 5版,并在发布说明中看到:

Swift语言和编译器
• 增量构建:仅更改函数主体不应再导致重建依赖文件。(15352929)

我试了一下,必须说现在它的工作效果非常好。他们在Swift中大大优化了增量构建。

我强烈建议您创建一个swift2.0分支,并使用XCode 7 beta 5保持代码更新。你会喜欢编译器的增强功能(不过我要说,XCode 7的全局状态仍然很慢和有缺陷)。

更新:使用Xcode 8.2

距离上次关于这个问题的更新已经有一段时间了,现在是时候更新一下了。
我们的应用程序现在有大约20k行几乎完全是Swift代码,这还算不错但也不是最出色的。它经历了swift 2和swift 3迁移。在干净的构建上,在中2014年Macbook pro(2.5 GHz英特尔Core i7)上编译大约需要5/6分钟。

但是即使苹果声称:

Xcode不会在发生小更改时重新构建整个目标。(28892475)

显然,当我们检查这个无聊的东西(向项目的任何文件添加一个私有属性(私有)!)后,我们都笑了...

我想指出一下Apple开发者论坛上的这个帖子,其中有一些关于此问题的更多信息(以及偶尔得到的赞赏的苹果开发者交流)。

基本上,人们想出了一些方法来尝试改善增量构建:

  1. 添加项目设置HEADER_MAP_USES_VFS并将其设置为true
  2. 从您的方案中禁用查找隐式依赖项
  3. 创建一个新项目并将文件层次结构移动到新项目中。

我将尝试解决方案3,但解决方案1/2对我们没有起作用。

在整个情况中真正具有讽刺意味的是,当我们遇到第一次编译缓慢的问题时,看着这个问题的第一篇帖子时,我们正在使用Xcode 6和我相信是Swift 1或Swift 1.1代码,现在大约两年过去了,尽管Apple有了实际改进,但情况与Xcode 6时一样糟糕。多么具有讽刺意味啊。

我真的非常后悔选择Swift而不是Obj / C来进行我们的项目,因为它带来了每日的挫败感。(我甚至转到了AppCode,但那是另一回事)

无论如何,我发现这个SO帖子在本文撰写时已经有32k+的浏览量和143个upvotes,所以我想我不是唯一一个遇到这种情况的人。坚持下去,尽管对这种情况感到悲观,但可能会有一些曙光。

如果您有时间(和勇气!),我想Apple欢迎有关此事的报告。

更新Xcode 9

今天偶然发现this。 Xcode悄悄引入了一个新的构建系统,以改善当前可怕的性能。您必须通过工作区设置启用它。

enter image description here

已经尝试过了,但在完成后将更新此帖子。看起来很有前途。


2
我曾经遇到过类似的问题,最终发现是因为实体类中使用了自定义运算符来从JSON反序列化。如果你也在使用这种方式,请尝试逐个将其转换为普通函数,并观察是否有任何变化。 - Antonio
4
自从 XCode 6 beta 6 以来,我的项目编译速度变得非常慢,让我感到非常焦虑。我不确定是因为 beta 版本的更改还是因为我的代码问题。尽管我的项目还没有变得很大(约有 40-50 个 Swift 文件)。 - BadmintonCat
2
随着我的项目规模的增长,编译变得无法忍受地缓慢。我还依赖于几个Pods,这肯定会加剧问题。这是使用最近的非beta版本。 - Andy
2
增量构建仍然采用“保守的依赖分析”,因此您可能仍会看到比绝对必要更多的文件重新构建。希望随着时间的推移会有所改善。 - nmdias
1
@ZigDanis 绝对不行,只有当XCode 7达到GM状态后,您才能提交应用程序到iTunes Connect。但是,您应该尽可能保持swift2更新。我注意到我的结尾短语很令人困惑,我已经更新了它以使其更清晰。 - apouche
显示剩余15条评论
22个回答

71

好的,结果证明Rob Napier是正确的。只有一个文件(实际上只有一个方法)会导致编译器变得疯狂。

不要误会。Swift确实会每次重新编译所有文件,但现在的好处是,苹果增加了对编译文件的实时编译反馈,因此Xcode 6 GM现在显示哪些Swift文件正在编译以及编译状态,正如您可以在此屏幕截图中看到的那样:

Enter image description here

这非常方便,可知道哪个文件需要花费更长时间。在我的情况下,就是这段代码:

var dic = super.json().mutableCopy() as NSMutableDictionary
dic.addEntriesFromDictionary([
        "url" : self.url?.absoluteString ?? "",
        "title" : self.title ?? ""
        ])

return dic.copy() as NSDictionary

因为属性title的类型为var title:String?而不是NSString。当将其添加到NSMutableDictionary中时,编译器会变得非常疯狂。

将其更改为:

var dic = super.json().mutableCopy() as NSMutableDictionary
dic.addEntriesFromDictionary([
        "url" : self.url?.absoluteString ?? "",
        "title" : NSString(string: self.title ?? "")
        ])

return dic.copy() as NSDictionary

让编译时间从10/15秒(甚至更多)降至1秒... 太神奇了。


4
谢谢您跟进答案。这可能对其他人追踪编译过程中类型推断引擎被卡住的情况非常有用。 - Rob Napier
1
你是从哪里得到这个视图的,@apouche?我在Xcode中没有看到它。 - Eric
2
您需要打开调试助手(CMD+8),然后点击当前构建。 - apouche
1
我相信苹果会后续优化,否则使用Swift开展真实项目将会受到影响。 - apouche
1
我该如何进入这个工具,以查看正在编译哪些文件? - jgvb
显示剩余7条评论

43
我们已经尝试了很多方法来解决这个问题,因为我们有大约100k行的Swift代码和300k行的ObjC代码。
我们的第一步是根据函数编译时间输出(例如在此处https://thatthinginswift.com/debug-long-compile-times-swift/中描述的内容)优化所有函数。
接下来,我们编写了一个脚本将所有Swift文件合并到一个文件中,这会破坏访问级别,但将编译时间从5-6分钟降至约1分钟。
现在这已经过时了,因为我们向苹果咨询了此事,他们建议我们执行以下操作:
1. 在“Swift编译器 - 代码生成”构建设置中打开“整个模块优化”。选择“快速、整个模块优化”。

enter image description here

  1. 'Swift编译器-自定义标志'中,对于您的开发构建,请添加'-Onone'

enter image description here enter image description here

当设置这些标志时,编译器将一次性编译所有Swift文件。我们发现,使用合并脚本比逐个编译文件要快得多。但是,如果没有'-Onone'覆盖,它也将优化整个模块,这会更慢。当我们在其他Swift标志中设置'-Onone'标志时,它会停止优化,但不会停止一次性编译所有Swift文件。
有关整个模块优化的更多信息,请查看苹果的博客文章 - https://swift.org/blog/whole-module-optimizations/ 我们发现这些设置使我们的Swift代码编译时间缩短到30秒 :-) 我没有证据表明它在其他项目上的工作情况,但如果Swift编译时间仍然是一个问题,我建议您尝试一下。
注意,对于您的应用商店构建,应省略'-Onone'标志,因为优化推荐用于生产构建。

4
非常感谢您的建议!我只是不明白为什么官方来源中没有类似的内容(至少很容易找到)。例如,您提到的文章应该(必须!)有关于-Onone的说明。我们现在无法使用整个模块优化,因为它会使编译器崩溃...但是您的建议让我们的构建速度几乎提高了10倍。在MacBook Air(2013年版)上,它的构建时间约为8分钟,现在已经降至约1分钟,其中一半时间用于在目标之间切换(我们有应用程序、扩展和几个内部框架)以及编译storyboard。 - Ilya Puchka
我也测试过这种方法,它是唯一提到 -Onone 的方法,并且它确实显著地减少了构建时间。 - Vlad
和我一起工作也可以。使用 -Onone 有助于减少构建时间。非常感谢,伙计! - nahung89

34

很可能与项目的大小无关,而是与某些特定的代码有关,甚至可能只有一行。您可以尝试逐个编译文件而不是整个项目来测试此问题。或者尝试观察构建日志以查看哪个文件花费了太长时间。

作为可能引起问题的代码类型的示例,这个38行代码片段在beta7中需要一分钟以上才能编译。 其中所有问题都是由这个代码块引起的:

let pipeResult =
seq |> filter~~ { $0 % 2 == 0 }
  |> sorted~~ { $1 < $0 }
  |> map~~ { $0.description }
  |> joinedWithCommas

只需简化为一两行即可,编译几乎瞬间完成。问题在于某些原因导致编译器呈指数增长(可能是阶乘增长)。显然这不是理想的情况,如果您能够隔离出这种情况,应该打开radar帮助解决这些问题。


我不确定你是否看到了我的关于“CompileSwift”阶段的评论。它会编译所有的Swift文件,即使只有一个被修改过。因此,如果只有一个文件需要一些时间(我非常怀疑),编译器将永远不会告诉你是哪个文件。 - apouche
11
你可以使用 swiftc 编译单个文件以查看其所需时间。 - Rob Napier
很抱歉我没有给你赏金,因为一开始我不相信它。我也尝试逐个编译文件,但这太麻烦了(每次都要正确地提供框架和依赖项),所以我放弃了。请查看我在此帖子中的最新答案以获取完整解释。 - apouche
我认为这并不是基于项目大小的问题。我的项目只有4个Swift文件,但最近开始编译非常缓慢。昨天它还极快。除了添加图标和启动图片之外,我不能确定我特别对我的项目做了什么。 - Travis M.

33

如果你想要找出导致编译时间变慢的具体文件,你可以尝试使用xctool通过命令行进行编译,这样会给你每个文件的编译时间。

需要注意的是,默认情况下它会并发构建每个CPU核心的2个文件,并不会给出“净”经过时间,而是绝对的“用户”时间。这样,所有并行化的文件的时间都是相似的。

为了解决这个问题,设置-jobs标志为1,这样就不会并行化文件的构建。虽然会花费更长的时间,但最终你会得到可以逐个比较文件的“净”编译时间。

以下是一个示例命令:

xctool -workspace <your_workspace> -scheme <your_scheme> -jobs 1 build

“编译Swift文件”阶段的输出将类似于:

...
    Compile EntityObserver.swift (1623 ms)
    Compile Session.swift (1526 ms)
    Compile SearchComposer.swift (1556 ms)
...

通过这个输出,您可以快速确定哪些文件编译时间比其他文件长。此外,您还可以准确判断您的重构(显式转换、类型提示等)是否会降低特定文件的编译时间。

注意:技术上您也可以使用xcodebuild,但其输出非常冗长且难以阅读。


1
请确保您的项目的整体模块优化设置为false,否则它将无法分离出单个的Swift文件。 - sabes
1
请查看 Swift编译器优化级别,以获取 快速、整个模块优化 [-O -whole-module-optimization] - Matt

31
在我的情况下,Xcode 7 没有任何改变。我有多个函数需要花费几秒钟才能编译。 示例
// Build time: 5238.3ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)

在去除可选项后,构建时间减少了99.4%

// Build time: 32.4ms
var padding: CGFloat = 22
if let rightView = rightView {
    padding += rightView.bounds.width
}

if let leftView = leftView {
    padding += leftView.bounds.width
}
return CGSizeMake(size.width + padding, bounds.height)

请查看这篇文章这篇文章以获取更多示例。

Xcode构建时间分析器

我开发了一个Xcode插件,可对遇到这些问题的任何人有所帮助。

image

Swift 3 中似乎会有改进,希望我们可以更快地编译 Swift 代码。


太棒了,希望我能给你超过+1的支持。你非常棒,你的插件也很棒。我已经使用过它了,我的构建时间正在缩短,这意味着开发速度超快,因为有时可选项是噩梦,会导致编译器变慢。 - hardikdevios
太棒了!你的工具帮了我很多忙。谢谢。 - Phil
很棒的插件 - 真的很有用!谢谢 - 365SplendidSuns
@Robert Gummesson,我们有没有针对Objective-C代码的工具? - Ashok

21

也许我们不能修复Swift编译器,但我们可以修复我们的代码!

在Swift编译器中有一个隐藏选项,它会输出编译器编译每个函数所需的精确时间间隔:-Xfrontend -debug-time-function-bodies。这使我们能够找到代码中的瓶颈,并显着提高编译时间。

只需在终端中运行以下命令并分析结果:

xcodebuild -workspace App.xcworkspace -scheme App clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep [1-9].[0-9]ms | sort -nr > culprits.txt

厉害的Brian Irace写了一篇关于此的精彩文章 Profiling your Swift compilation times.


2
对于那些使用zsh的人,首先要执行alias grep='noglob grep',否则grep将无法工作。 - Jaime Agudo

16

解决方案是投掷。

我有一大堆字典,像这样:

["title" : "someTitle", "textFile" : "someTextFile"],
["title" : "someTitle", "textFile" : "someTextFile"],
["title" : "someTitle", "textFile" : "someTextFile"],
["title" : "someTitle", "textFile" : "someTextFile"],
.....

编译花费了大约40分钟。直到我像这样转换字典:

["title" : "someTitle", "textFile" : "someTextFile"] as [String : String],
["title" : "someTitle", "textFile" : "someTextFile"] as [String : String],
["title" : "someTitle", "textFile" : "someTextFile"] as [String : String],
....

这在我应用程序中硬编码的数据类型所遇到的几乎所有其他问题上都发挥了作用。


6
是的,这是优化编译时间所做的一部分,但当前Swift编译器的主要问题仍然是,每当您进行最轻微的修改时,它仍会重新编译每个单独的Swift文件。 - apouche
4
如果不是这么悲哀,那就有趣了。 - Tom Andersen

15

需要注意的一点是,Swift类型推断引擎在处理嵌套类型时可能会非常缓慢。您可以通过观察编译单元的构建日志并复制和粘贴完整的Xcode生成的命令到终端窗口中,然后按CTRL-\获得一些诊断信息,从而对造成缓慢的原因有一个大致的了解。请参阅http://blog.impathic.com/post/99647568844/debugging-slow-swift-compile-times 获取完整示例。


这对我来说是最好的答案(请参见链接)。我可以轻松地找到出现问题的两行不同代码,并通过将其分解为更小的行来解决它。 - Nico
这是一个非常有用的答案,因为它展示了如何找到编译器出错的位置。在我的情况下,问题出在以下代码上: 'curScore[curPlayer%2]+curScore[2+curPlayer%2]==3 && maker%2==curPlayer%2' 当我将其从“if”移动到“let”时,就会出现“表达式过于复杂,无法在合理的时间内解决;请考虑将表达式分解为不同的子表达式”的结果。 - Dmitry
这绝对是解决这个问题最有帮助的方法。 - Richard Venable

9
确保在调试编译时(无论是Swift还是Objective-C),将“仅构建活动架构”设置为开启状态:

enter image description here


7
由于所有这些都在Beta版中,而且Swift编译器(至少到今天为止)并没有开源,我想你的问题没有一个真正的答案。首先,将Objective-C与Swift编译器进行比较有点残酷。Swift仍处于Beta阶段,我相信苹果正在努力提供功能和修复错误,而不是提供闪电般的速度(你不会从购买家具开始建造房屋)。我猜想苹果将及时优化编译器。如果由于某种原因需要编译所有源文件,一个选项可能是创建分离的模块/库。但是,目前还不存在这个选项,因为在语言稳定之前,Swift不能允许使用库。我的猜测是他们将优化编译器。出于同样的原因,我们无法创建预编译的模块,很可能编译器需要从头开始编译所有东西。但一旦语言达到稳定版本并且二进制文件的格式不再改变,我们就能够创建自己的库,也许(?)编译器也能够优化它的工作。只是猜测,因为只有苹果知道...

由于我们无法创建预编译模块,同样的原因是编译器可能需要从头开始编译所有内容。这是一个很好的观察,以前没有想过。 - chakrit
1
2017年了,它仍然很慢。 - Pedro Paulo Amorim
2017年使用Xcode 9和新构建系统,但仍然很慢。 - pableiros
在2018年使用Xcode 9,我有一个包含50多个Swift文件的项目,如果我进行清理构建,现在已经过了5分钟,但我的编译还没有完成。 - Chen Li Yong

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接