为什么Swift编译时间这么慢?
我正在使用Xcode 6 Beta 6。
这是一段时间以来一直在困扰我的东西,但现在已经到了几乎没有用的地步。
我的项目开始有一个体面的65个Swift文件和几个桥接的Objective-C文件(这实际上不是问题的原因)。
看起来像任何Swift文件的轻微修改(例如,在应用程序中几乎没有使用的类中添加一个简单的空白)将导致重新编译指定目标的整个Swift文件。
经过深入的调查,我发现几乎100%的编译时间是CompileSwift
阶段,其中Xcode在目标的所有Swift文件上运行swiftc
命令。
我做了一些进一步的调查,如果我只保留一个默认的控制器的应用程序委托编译是非常快,但是当我添加越来越多的我的项目文件,编译时间开始变得非常慢。
现在只有65个源文件,每次编译需要大约8/10秒。 一点都不快 。
除了这个之外,我还没有看到有关这个问题的任何文章,但它是Xcode 6的旧版本。所以我想知道在这种情况下是否是唯一的。
UPDATE
我已经在GitHub上检查了一些像Alamofire , Euler和CryptoSwift这样的Swift项目,但是他们没有足够的Swift文件进行实际的比较。 我发现唯一一个开始有一个体面的大小的项目是SwiftHN ,即使它只有一打源文件,我仍然能够validation同样的事情,一个简单的空间和整个项目需要重新编译,开始采取很less的时间(2/3秒)。
与分析器和汇编都快速的Objective-C代码相比,Swift永远无法处理大型项目,但请告诉我,我错了。
更新与Xcode 6 Beta 7
仍然没有任何改善。 这开始变得荒谬。 由于缺乏Swift中的#import
,我真的不知道苹果怎么能够优化这个。
更新与Xcode 6.3和Swift 1.2
苹果已经增加了增量构build (以及许多其他编译器优化)。 您必须将代码迁移到Swift 1.2才能看到这些好处,但Apple在Xcode 6.3中添加了一个工具来帮助您:
然而
不要像我那样快乐。 他们用来增加构build的图解算器还没有很好的优化。
事实上,首先,它不会查看函数签名的变化,所以如果在一个方法的块中添加一个空格,所有依赖于该类的文件都将被重新编译。
其次,它似乎是基于重新编译的文件创build树,即使更改不影响它们。 例如,如果您将这三个类移到不同的文件中
class FileA: NSObject { var foo:String? } class FileB: NSObject { var bar:FileA? } class FileC: NSObject { var baz:FileB? }
现在,如果您修改FileA
,编译器显然FileA
标记为重新编译。 它也将重新编译FileB
(基于对FileA
的更改可以FileA
), 而且 FileB
也是重新编译的,这是非常糟糕的,因为FileC
从来没有在这里使用FileA
。
所以我希望他们改进依赖树解决scheme…我用这个示例代码打开了一个雷达 。
更新与Xcode 7testing版5和Swift 2.0
苹果昨天发布了testing版5,在发行说明中我们可以看到:
Swift语言和编译器•增量构build:只改变一个函数的主体不应该导致依赖文件被重build。 (15352929)
我已经尝试了一下,我必须说现在它真的(真的!)正在工作。 他们极大地优化了快速增量构build。
我强烈build议你创build一个swift2.0
分支,并使用XCode 7 beta 5保持你的代码是最新的。你会很高兴的是编译器的增强(不过我想说的XCode 7的全球状态仍然缓慢&越野车)
更新与Xcode 8.2
自从我在这个问题上的最新更新已经有一段时间了,所以在这里。
我们的应用程序现在大约是20万行Swift代码,虽然体面但不突出。 它经历了迅速的2次和快速的3次迁移。 在2014年年中的Macbook pro(2.5 GHz Intel Core i7)上编译大概需要5 / 6m左右的时间。
然而,尽pipe苹果宣称:增量构build仍然是一个笑话:
当只有很小的变化发生时,Xcode不会重build整个目标。 (28892475)
很明显,我想我们中的许多人只是在检查了这个废话之后才笑了(为我的项目的任何文件添加一个私有(private!)属性将重新编译整个事物…)
我想指出你们在苹果开发者论坛上的这个主题 , 这个主题有更多的关于这个问题的信息(以及在一段时间内赞赏苹果开发者关于这个问题的交stream)
基本上人们已经想出了几件事来尝试改进增量构build:
- 添加一个
HEADER_MAP_USES_VFS
项目设置为true
- 禁用从您的scheme中
Find implicit dependencies
- 创build一个新的项目,并将您的文件层次结构移动到新的项目。
我会尝试解决scheme3,但解决scheme1/2不适合我们。
在这个问题上有讽刺意味的是,在第一篇文章中,我们使用了Xcode 6,当我们达到第一次编译的时候,我们相信Swift1或者Swift1.1的代码,现在大约两年后,尽pipe苹果公司情况和Xcode 6一样糟糕。真是讽刺。
我真的很后悔selectSwift over Obj / C作为我们的项目,因为它涉及到每天的挫败感。 (我甚至切换到AppCode,但这是另一回事)
无论如何,我看到这个SOpost有32k +的意见和143 ups本文写作,所以我想我不是唯一的。 尽pipe对这种情况持悲观态度,但在这里仍然存在,在隧道尽头可能会有一些亮光。
如果你有时间(和勇气!)我想苹果欢迎这个雷达。
直到下次! 干杯
更新与Xcode 9
今天绊倒。 Xcode悄然推出了一个新的构build系统,以改善目前糟糕的性能。 您必须通过工作区设置启用它。
已经尝试过,但将完成后更新此post。 看起来很有希望。
那么,结果是Rob Napier是对的。 这是一个单一的文件(实际上是一种方法),导致编译器去berzek。
现在不要误会我的意思。 Swift每次都会重新编译所有的文件,但现在最重要的是,苹果公司在编译的文件上join了实时编译反馈,所以Xcode 6 GM现在可以显示哪些Swift文件正在编译,编译的状态是实时的正如你在这个截图中看到的那样:
所以这很方便知道哪些文件需要这么长时间。 在我的情况下,这是这段代码:
var dic = super.json().mutableCopy() as NSMutableDictionary dic.addEntriesFromDictionary([ "url" : self.url?.absoluteString ?? "", "title" : self.title ?? "" ]) return dic.copy() as NSDictionary
因为属性title
的types是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秒(甚至更多)下降到一秒钟…惊人的。
我们已经尝试了很多东西来解决这个问题,因为我们有大约10万行Swift代码和30万行ObjC代码。
我们的第一步是根据函数编译时间输出来优化所有的函数(例如https://thatthinginswift.com/debug-long-compile-times-swift/ )
接下来,我们编写了一个脚本来将所有的swift文件合并到一个文件中,这就打破了访问级别,但是它将我们的编译时间从5-6min缩短到了1min。
现在已经不存在了,因为我们询问了苹果,他们build议我们应该做以下事情:
- 在“Swift编译器 – 代码生成”构build设置中打开“整个模块优化”。 select
'Fast, Whole Module Optimization'
- 在“Swift编译器 – 自定义标志”中,为您的开发版本添加
'-Onone'
当这些标志被设置时,编译器将在一个步骤中编译所有的Swift文件。 我们用我们的合并脚本发现这比单独编译文件要快得多。 但是,如果没有“ -Onone'
覆盖,它也会优化整个模块,这是较慢的。 当我们在其他Swift标志中设置'-Onone'
标志时,它会停止优化,但不会停止一步编译所有Swift文件。
有关整个模块优化的更多信息,请查看Apple的博客文章 – https://swift.org/blog/whole-module-optimizations/
我们发现这些设置允许我们的Swift代码在30秒内编译:-)我没有证据表明它将如何在其他项目上工作,但是如果Swift编译时间对您来说仍然是一个问题,我build议尝试一下。
请注意,您的app store构build应该保留'-Onone'
标志,因为build议对生产版本进行优化。
如果您正在尝试识别会减慢编译时间的特定文件,则可以尝试通过xctool从命令行进行编译 ,这会为您提供逐个文件的编译时间。
需要注意的是,默认情况下,它会为每个CPU内核同时创build2个文件,而不会给你“净”stream逝的时间,而是绝对的“用户”时间。 这样所有的时间都在并行文件之间,看起来非常相似。
为了克服这个问题, 请将-jobs
标志设置为1 ,以便它不会并行化文件构build。 这将花费更长的时间,但最后你会有“净”的编译时间,你可以逐个比较文件。
这是一个应该诀窍的示例命令:
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) ...
从这个输出中,您可以快速确定哪些文件比其他文件编译时间要长。 而且,您可以高精度地确定您的重构(显式types转换,types提示等)是否正在降低特定文件的编译时间。
注意:从技术上讲,你也可以用xcodebuild
但是输出结果非常详细,很难使用。
这可能与您的项目规模无关。 这可能是一些特定的代码,甚至可能只是一行。 您可以通过尝试一次编译一个文件而不是整个项目来进行testing。 或者试着看看构build日志,看看哪个文件需要这么长时间。
作为可能导致麻烦的代码types的一个例子, 这个38行要点需要1分多钟才能在beta7中编译。 所有这一切都是由这一块造成的:
let pipeResult = seq |> filter~~ { $0 % 2 == 0 } |> sorted~~ { $1 < $0 } |> map~~ { $0.description } |> joinedWithCommas
通过一两行简化,几乎立即编译。 麻烦是这样的,在编译器中引起了指数增长(可能是因子增长)。 显然这不是很理想,如果你能隔离这种情况,你应该打开雷达来帮助清理这些问题。
在我的情况下,Xcode 7没有任何区别。 我有多个函数需要几秒钟的编译。
例
// Build time: 5238.3ms return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)
在解开可选项后,构build时间下降了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构build时间分析器
我开发了一个Xcode插件 ,可以为遇到这些问题的任何人提供帮助。
Swift 3中似乎有了一些改进,希望我们可以更快地看到我们的Swift代码。
解决scheme是铸造。
我有一大堆字典,像这样:
["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], ....
这几乎适用于我遇到的数据types,我硬编码到我的应用程序的其他所有问题。
有一件事要注意的是,Swifttypes推理引擎可以非常缓慢的嵌套types。 您可以通过观察需要很长时间的单个编译单元的构build日志,然后将完整的Xcode-spawned命令复制并粘贴到terminal窗口,然后点击CTRL-\来获得有关导致缓慢的一般想法一些诊断。 看一个完整的例子http://blog.impathic.com/post/99647568844/debugging-slow-swift-compile-times 。
可能我们不能修复Swift编译器,但是我们可以修复的是我们的代码!
在Swift编译器中有一个隐藏的选项,它打印出编译器编译每一个函数所需的确切时间间隔: -Xfrontend -debug-time-function-bodies
。 它使我们能够在代码中find瓶颈,并显着缩短编译时间。
在terminal简单运行以下内容并分析结果:
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写了关于它的精彩文章分析你的Swift编译时间 。
还要确保在编译debugging时(Swift或Objective-C),设置为仅生成活动架构:
由于所有这些东西都在testing版中,并且由于Swift编译器(至less在今天)没有打开,所以我猜这个问题没有真正的答案。
首先,将Objective-C与Swift编译器进行比较是非常残酷的。 Swift仍然处于testing阶段,我相信苹果公司正在努力提供function和修复错误,而不仅仅是提供闪电般的速度(你不会通过购买家具开始build造房屋)。 我想苹果会在适当的时候优化编译器。
如果由于某种原因,所有的源文件都必须编译完整,一个选项可能是创build分离的模块/库。 但是这个选项是不可能的,因为Swift在语言稳定之前不能允许库。
我的猜测是他们会优化编译器。 出于同样的原因,我们不能创build预编译的模块,很可能编译器需要从头开始编译所有的东西。 但是,一旦语言达到稳定版本,二进制文件的格式不再变化,我们就可以创build我们的库,也许编译器也可以优化它的工作。
只是猜测,只有苹果知道…
对于Xcode 8,进入项目设置,然后编辑器>添加生成设置>添加用户定义的设置,并添加以下内容:
SWIFT_WHOLE_MODULE_OPTIMIZATION = YES
添加这个标志,我们的清理编译时间从7分钟到65秒,奇迹般地进行了40KLOC的快速工程。 也可以确认2个朋友在企业项目上看到了类似的改进。
我只能假设这是Xcode 8.0中的某种错误
编辑:它似乎没有工作了Xcode 8.3中的一些人。
不幸的是,Swift编译器仍然没有针对快速和增量编译进行优化(从Xcode 6.3 beta开始)。 同时你可以使用下面的一些技巧来提高Swift的编译时间:
-
将应用程序拆分为Frameworks以减less重新编译的影响。 但请注意,您必须避免在应用程序中出现循环依赖关系。 有关此主题的更多信息,请查看此帖: http : //bits.citrusbyte.com/improving-swift-compile-time/
-
使用Swift来完成项目中相当稳定的部分,而且不要经常更改。 对于需要频繁更改的其他领域或需要大量编译/运行迭代才能完成的领域(几乎所有与UI相关的东西),最好使用Objective-C的混合搭配方法。
-
尝试运行时代码注入'注入Xcode'
-
使用roopc方法: http ://roopc.net/posts/2014/speeding-up-swift-builds/
-
通过给出一些明确的演员提示缓解快速types推理引擎。
Swift数组和字典的构造似乎是一个非常受欢迎的原因(特别是对于来自Ruby背景的人),也就是说,
var a = ["a": "b", "c": "d", "e": "f", "g": "h", "i": "j", "k": "l", "m": "n", "o": "p", "q": "r", "s": "t", "u": "v", "x": "z"]
可能是这个问题应该解决的原因:
var a = NSMutableDictionary() a["a"] = "b" a["c"] = "d" ... and so on
对于debugging和testing,请确保使用以下设置将编译时间从大约20分钟缩短到less于2分钟,
- 在项目构build设置中,search“优化”将debugging设置为“最快[-O3]”或更高。
- 为活动架构设置构build:是
- debugging信息格式:DWARF
- 整个模块优化:NO
我浪费了无数个小时,等待着这个项目的build立,才意识到我必须做出这样一个小小的改变,不得不等待整整30分钟来testing一下。 这些是为我工作的设置。 (我仍在试验设置)
但是,确保至less将“DWARF with dSYM”(如果要监视应用程序)和“将活动体系结构构build为”NO(发布/归档)推到iTunes Connect(我记得在这里浪费了几个小时)。
重新启动我的Mac为这个问题做了奇迹。 我通过重新启动从15分钟到30秒。
编译器花费大量的时间来推断和检查types。 所以添加types注释有助于编译器很多。
如果你有很多链式函数调用
let sum = [1,2,3].map({String($0)}).flatMap({Float($0)}).reduce(0, combine: +)
然后,编译器需要一段时间来找出sum
的types应该是什么。 添加types有帮助。 将间歇步骤拉入单独的variables也有帮助。
let numbers: [Int] = [1,2,3] let strings: [String] = sum.map({String($0)}) let floats: [Float] = strings.flatMap({Float($0)}) let sum: Float = floats.reduce(0, combine: +)
特别是对于数字typesCGFloat
, Int
可以帮助很多。 类似2
的文字数字可以表示许多不同的数字types。 所以编译器需要从上下文中找出它是哪一个。
也需要避免花费大量时间来查找的function。 使用几个+
连接几个数组是很慢的,因为编译器需要找出每个+
应该调用哪个+
实现。 因此,如果可能,请使用var a: [Foo]
和append()
。
你可以添加一个警告来检测哪些函数在Xcode中编译速度慢 。
在生成设置为您的目标search其他Swift Flags并添加
-Xfrontend -warn-long-function-bodies=100
警告每个需要超过100毫秒的函数才能编译。
对于混合Objective-C和Swift代码的项目,我们可以在Other Swift Flags
设置-enable-bridging-pch
。 有了这个,桥头只被parsing一次,并且结果(一个临时的“预编译的头文件”或“PCH”文件)在目标中的所有Swift文件中被caching和重用。 苹果声称它减less了30%的构build时间。 参考链接:
注意:这适用于Swift 3.1及更高版本。
Swift编译时间在新的Xcode 6.3中得到了改进
编译器改进
Swift 1.2编译器被devise为更稳定,并以各种方式提高性能。 在Xcode中使用Swift时,这些更改也提供了更好的体验。 一些最明显的改进包括:
增量构build
没有更改的源文件将不再被默认重新编译,这将大大改善大多数常见情况下的生成时间。 对代码进行更大的结构更改可能仍然需要重build多个文件。
更快的可执行文件
debugging生成运行速度更快的二进制文件,新的优化提供了更好的发布生成性能。
更好的编译器诊断
更清晰的错误和警告消息,以及新的Fix-it,使编写适当的Swift 1.2代码更容易。
稳定性改进
最常见的编译器崩溃已经修复。 您应该在Xcode编辑器中看到更less的SourceKit警告。
这是另一个可能导致types推断大幅下降的情况。 合并运营商 。
换行如下:
abs(some_optional_variable ?? 0)
至
abs((some_optional_variable ?? 0) as VARIABLE_TYPE)
帮助我的编译时间从70年代到13年代
在Xcode 6.3.1中没有任何工作 – 当我添加了100个Swift文件时,Xcode随机在构build和/或索引上挂起。 我试过了一个没有成功的模块化选项。
安装和使用Xcode 6.4 Beta实际上为我工作。
这对我来说就像魔术一样 – 加速Swift编译 。 它将编译时间从10分钟缩短到3分钟。
它说你应该打开Whole Module Optimization
而在Other Swift Flags
添加-Onone
。
我在Xcode 8.3
/ Xcode 8.2
上使用Swift 3
。