关于编译:为什么Swift编译时间这么慢?

Why is Swift compile time so slow?

我用的是Xcode6测试版6。

这是困扰我一段时间的事情,但现在它已经到了一个几乎不可用的地步。

我的项目开始有一个相当大的65个Swift文件和一些桥接的Objective-C文件(这实际上不是问题的原因)。

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

经过更深入的调查,我发现几乎100%的编译器时间都是在CompileSwift阶段,xcode在目标的所有swift文件上运行swiftc命令。

我做了一些进一步的调查,如果我只使用默认的控制器保存应用程序委托,编译速度会非常快,但是随着我添加了越来越多的项目文件,编译时间开始变得非常慢。

现在只有65个源文件,每次编译大约需要8/10秒。一点也不太快。

除了这个问题,我没有看到任何关于这个问题的文章,但它是Xcode6的旧版本。所以我想知道我是不是唯一一个这样的人。

更新

我在Github上检查过一些Swift项目,比如Alamoire、Euler和CryptoSwift,但没有一个项目有足够的Swift文件来进行实际比较。我发现的唯一一个项目是Swifhn,虽然它只有十几个源文件,但我仍然能够验证相同的东西,一个简单的空间和整个项目需要重新编译,这开始需要一点时间(2/3秒)。

与分析程序和编译速度都很快的Objective-C代码相比,这真的让人觉得Swift永远无法处理大项目,但请告诉我我错了。

用xcode 6 beta 7更新

仍然没有任何改善。这开始变得荒谬起来。由于缺乏迅捷的#import,我真的不知道苹果将如何优化这一点。

使用xcode 6.3和swift 1.2更新

苹果已经添加了增量编译(以及许多其他编译器优化)。您必须将代码迁移到swift 1.2才能看到这些好处,但Apple在Xcode6.3中添加了一个工具来帮助您做到这一点:

Enter image description here

然而

别像我一样高兴得太快。他们用来使构建增量的图求解器还没有很好地优化。

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

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

1
2
3
4
5
6
7
8
9
class FileA: NSObject {
    var foo:String?
}
class FileB: NSObject {
    var bar:FileA?
}
class FileC: NSObject {
    var baz:FileB?
}

现在,如果修改FileA,编译器显然会将FileA标记为要重新编译。它还将重新编译FileB(根据对FileA的更改可以),也将重新编译FileC,因为FileB是重新编译的,这是非常糟糕的,因为FileC在这里从未使用FileA

所以我希望他们能改进依赖树解算器…我用这个样本码打开了雷达。

使用xcode 7 beta 5和swift 2.0更新

昨天苹果发布了测试版5,在发布说明中我们可以看到:

Swift Language & Compiler
? Incremental builds: changing just the body of a function should no longer cause dependent files to be rebuilt. (15352929)

我试过了,我必须说它真的(真的)起作用了。现在好了。他们大大优化了快速增量构建。

我强烈建议您创建一个swift2.0分支,并使用xcode 7测试版5保持代码的最新。您会对编译器的增强感到满意(不过,我要说的是Xcode7的全局状态仍然很慢而且有问题)

用xcode 8.2更新

我最近一次更新这个问题已经有一段时间了,所以这里是。

我们的应用程序现在大约有2万行几乎完全是Swift代码,这是体面的,但并不突出。它经历了斯威夫特2号和斯威夫特3号的迁徙。在2014年年中的MacBook Pro(2.5 GHz Intel Core i7)上编译大约需要5/6百万次,这在一个干净的版本上是可以接受的。

然而,尽管苹果声称:

Xcode will not rebuild an entire target when only small changes have occurred. (28892475)

很明显,我想我们中的很多人在看了这些胡说八道之后都笑了(增加了一个私人的(私人的!)属性到我的项目的任何文件都将重新编译整个项目…)

我想向你们指出苹果开发者论坛上的这个主题,它有关于这个问题的更多信息(同时也感谢苹果开发者偶尔就此事进行交流)

基本上,人们想出了一些改进增量构建的方法:

  • HEADER_MAP_USES_VFS项目设置集添加到true中。
  • 从您的sch中禁用Find implicit dependencies


    原来罗布·纳皮尔是对的。只有一个文件(实际上是一个方法)导致编译器疯狂。

    现在别误会我。Swift每次都会重新编译您的所有文件,但现在最重要的是,苹果对其编译的文件添加了实时编译反馈,因此Xcode 6 gm现在显示正在编译的Swift文件以及实时编译的状态,如您在屏幕截图中看到的:

    Enter image description here

    因此,这非常方便地知道哪些文件需要这么长时间。在我的例子中,这是一段代码:

    1
    2
    3
    4
    5
    6
    7
    var dic = super.json().mutableCopy() as NSMutableDictionary
    dic.addEntriesFromDictionary([
           "url" : self.url?.absoluteString ??"",
           "title" : self.title ??""
            ])

    return dic.copy() as NSDictionary

    因为财产titlevar title:String?型,而不是NSString型。当把它添加到NSMutableDictionary时,编译器变得疯狂了。

    将其改为:

    1
    2
    3
    4
    5
    6
    7
    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秒(甚至更多)下降到一秒…太神了。


    我们已经尝试了很多事情来解决这个问题,因为我们有大约100K行的swift代码和300K行的objc代码。

    我们的第一步是根据函数编译时输出优化所有函数(如这里所述https://thatthinginswift.com/debug-long-compile-times-swift/)

    接下来,我们编写了一个脚本,将所有swift文件合并到一个文件中,这打破了访问级别,但它将我们的编译时间从5-6分钟缩短到了大约1分钟。

    现在这已经不起作用了,因为我们向苹果询问了这一点,他们建议我们应该采取以下措施:

  • 在"swift编译器-代码生成"构建设置中启用"整个模块优化"。选择'Fast, Whole Module Optimization'
  • enter image description here

  • 在"swift compiler-custom flags"中,为开发构建添加'-Onone'
  • enter image description hereenter image description here

    设置这些标志后,编译器将在一个步骤中编译所有swift文件。我们发现,使用合并脚本,这比单独编译文件快得多。但是,如果没有'-Onone'超控,它也将优化整个模块,速度较慢。当我们在其他swift标志中设置'-Onone'标志时,它会停止优化,但不会在一个步骤中停止编译所有swift文件。

    有关整个模块优化的更多信息,请查看Apple的博客帖子-https://swift.org/blog/whole-module-optimization s/

    我们发现这些设置允许我们的Swift代码在30秒内编译:-)我没有证据证明它在其他项目中如何工作,但我建议如果Swift编译时间仍然是您的问题,请尝试一下。

    注意:对于你的应用程序商店版本,你应该去掉'-Onone'标记,因为生产版本推荐优化。


    如果您试图识别特定的文件来降低编译时间,那么您可以尝试通过XCTOOL从命令行编译它,XCTOOL将为您提供一个文件一个文件的编译时间。

    需要注意的是,默认情况下,它为每个CPU核心并发地构建2个文件,并且不会给您"净"运行时间,而是绝对的"用户"时间。这样所有的时间安排在并行文件之间就显得非常相似了。

    要克服这个问题,请将-jobs标志设置为1,这样它就不会并行文件构建。这将花费更长的时间,但最终您将有"net"编译时间,您可以逐文件比较。

    这是一个示例命令,应该实现以下功能:

    xctool -workspace -scheme -jobs 1 build

    "编译swift文件"阶段的输出如下:

    1
    2
    3
    4
    5
    ...
       ? Compile EntityObserver.swift (1623 ms)
       ? Compile Session.swift (1526 ms)
       ? Compile SearchComposer.swift (1556 ms)
    ...

    从这个输出中,您可以快速地确定哪些文件比其他文件要花更长的时间来编译。此外,您可以高精度地确定重构(显式强制转换、类型提示等)是否降低了特定文件的编译时间。

    注意:从技术上讲,您也可以使用xcodebuild来完成它,但是输出非常冗长,而且很难消耗。


    这可能与你的项目规模无关。它可能是一些特定的代码,甚至可能只是一行。您可以通过尝试一次编译一个文件而不是整个项目来测试这一点。或者尝试查看生成日志以查看哪个文件需要这么长时间。

    作为可能导致问题的代码类型的一个例子,这个38行的gist在beta7中编译需要一分钟多的时间。所有这些都是由这一块引起的:

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

    将其简化为一两行,它几乎可以立即编译。问题在于,这会导致编译器中的指数增长(可能是阶乘增长)。显然,这并不理想,如果你能隔离这种情况,你应该打开雷达来帮助清理这些问题。


    在我的例子中,xcode 7根本没有区别。我有多个函数需要几秒钟来编译。

    例子

    1
    2
    // Build time: 5238.3ms
    return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)

    解包选项后,构建时间下降了99.4%。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 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

    在Swift3中似乎有一些改进,所以希望我们能看到我们的Swift代码编译得更快。


    解决方案是铸造。

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

    1
    2
    3
    4
    5
    ["title" :"someTitle","textFile" :"someTextFile"],
    ["title" :"someTitle","textFile" :"someTextFile"],
    ["title" :"someTitle","textFile" :"someTextFile"],
    ["title" :"someTitle","textFile" :"someTextFile"],
    .....

    编译它大约需要40分钟。直到我把字典打成这样:

    1
    2
    3
    4
    ["title" :"someTitle","textFile" :"someTextFile"] as [String : String],
    ["title" :"someTitle","textFile" :"someTextFile"] as [String : String],
    ["title" :"someTitle","textFile" :"someTextFile"] as [String : String],
    ....

    这几乎解决了我在应用程序中硬编码的数据类型方面遇到的所有其他问题。


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

    Swift编译器中有一个隐藏的选项,它打印出编译器编译每个函数所需的确切时间间隔:-Xfrontend -debug-time-function-bodies。它允许我们在代码中找到瓶颈并显著地提高编译时间。

    在终端中简单运行以下内容并分析结果:

    1
    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

    令人敬畏的布莱恩·伊拉斯写了一篇关于它的精彩文章,描述了你快速编译的时代。


    需要注意的一点是,对于嵌套类型,swift类型推理引擎可能非常慢。您可以通过查看耗时较长的单个编译单元的生成日志,然后将生成的完整xcode命令复制并粘贴到终端窗口中,然后点击ctrl-以获取一些诊断信息,从而大致了解导致速度慢的原因。请参阅http://blog.impathic.com/post/99647568844/debugging-slow-swift-compile-times了解完整的示例。


    还要确保在为调试编译时(swift或objective-c),您设置为仅构建活动体系结构:

    enter image description here


    因为所有这些东西都是测试版的,而且由于Swift编译器(至少在今天)还没有打开,我想你的问题没有真正的答案。

    首先,比较Objective-C和Swift编译器有点残忍。斯威夫特仍在测试阶段,我相信苹果正在努力提供功能和修复漏洞,而不仅仅是提供闪电般的速度(你不会从购买家具开始建造房屋)。我想苹果会在适当的时候优化编译器。

    如果出于某种原因,所有源文件都必须编译完成,那么可以选择创建单独的模块/库。但这一选择目前还不可能实现,因为在语言稳定之前,Swift不允许使用库。

    我猜他们会优化编译器。出于同样的原因,我们不能创建预编译的模块,很可能是编译器需要从头开始编译所有东西。但是一旦语言达到一个稳定的版本,二进制文件的格式不再改变,我们就可以创建库,也许(?)编译器还可以优化其工作。

    只是猜测,因为只有苹果知道…


    对于Xcode 8,转到"项目设置",然后转到"编辑器>添加生成设置>添加用户定义设置",然后添加以下内容:

    1
    SWIFT_WHOLE_MODULE_OPTIMIZATION = YES

    添加这个标志,我们的清洁构建编译时间从7分钟下降到65秒,为一个40klocswift项目,奇迹般。也可以确认两个朋友在企业项目上看到了类似的改进。

    我只能假设这是Xcode8.0中的某种bug

    编辑:在Xcode8.3中,有些人似乎不再使用它了。


    对于调试和测试,请确保使用以下设置将编译时间从大约20分钟缩短到少于2分钟,

  • 在项目生成设置中,搜索"优化"将调试转到"最快的[-o3]或更高版本。
  • 为活动体系结构设置生成:是
  • 调试信息格式:DWARF
  • 整个模块优化:否
  • 我浪费了无数的时间等待项目的建设,结果我意识到我必须做一个小小的改变,不得不再等30分钟来测试它。这些是适合我的设置。(我仍在尝试设置)

    但是,请确保您至少将"dwarf with dsym"(如果您想监视您的应用程序)设置为"no",并将活动体系结构构建为"no",以便发布/归档以推送至iTunes Connect(我还记得在这里浪费了几个小时)。


    Swift数组和字典结构似乎是一个非常流行的原因(特别是对于来自Ruby背景的用户),也就是说,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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"]

    这可能是解决问题的原因:

    1
    2
    3
    4
    var a = NSMutableDictionary()
    a["a"] ="b"
    a["c"] ="d"
    ... and so on

    不幸的是,Swift编译器还没有针对快速和增量编译进行优化(从Xcode6.3测试版开始)。同时,您可以使用以下一些技术来提高快速编译时间:

    • 将应用程序拆分为框架以减少重新编译的影响。但要注意,在应用程序中必须避免循环依赖。有关此主题的更多信息,请查看以下文章:http://bits.cirtusbyte.com/improving-swift-compile-time/

    • 对项目中相当稳定且不经常更改的部分使用swift。对于需要经常更改的其他区域或需要完成大量编译/运行迭代的区域(几乎所有与UI相关的内容),最好使用带有混合匹配方法的Objective-C。

    • 尝试使用"为Xcode注入"进行运行时代码注入

    • 使用roopc方法:http://roopc.net/posts/2014/speeding-up-swift-builds/

    • 通过给出一些带有明确类型转换的提示来释放swift类型推理引擎。


    编译器花费大量时间来推断和检查类型。因此添加类型注释对编译器有很大帮助。

    如果您有很多像这样的链接函数调用

    1
    let sum = [1,2,3].map({String($0)}).flatMap({Float($0)}).reduce(0, combine: +)

    然后编译器需要一段时间来确定sum的类型。添加类型有帮助。还有助于将间歇步骤拉入单独的变量中。

    1
    2
    3
    4
    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: +)

    特别是对于数字类型CGFloatInt有很大帮助。像2这样的文字数字可以表示许多不同的数字类型。所以编译器需要从上下文中找出它是哪一个。

    也应该避免使用像+这样需要花费大量时间查找的函数。使用多个+连接多个数组很慢,因为编译器需要找出每个+应该调用哪个+实现。因此,如果可能的话,用带append()var a: [Foo]代替。

    您可以添加一个警告来检测哪些函数在Xcode中编译缓慢。

    在目标的生成设置中,搜索其他Swift标志并添加

    -Xfrontend -warn-long-function-bodies=100

    对编译时间超过100 ms的每个函数发出警告。


    重新启动我的Mac对这个问题很有帮助。我只是通过重新启动从15分钟的构建到30秒的构建。


    对于Objective-C和Swift代码混合的项目,我们可以在Other Swift Flags中设置-enable-bridging-pch。这样,桥接头只解析一次,结果(临时的"预编译头"或"pch"文件)被缓存并在目标中的所有swift文件中重用。苹果声称它将生产时间缩短了30%。参考链接:

    注:仅适用于Swift 3.1及以上版本。


    新Xcode 6.3改进了swift编译时间

    Compiler improvements

    The Swift 1.2 compiler was engineered to be more stable and to improve
    performance in every way. These changes also provide a better
    experience when working with Swift in Xcode. Some of the most visible
    improvements include:

    Incremental builds

    Source files that haven’t changed will no longer be re-compiled by
    default, which will significantly improve build times for most common
    cases. Larger structural changes to your code may still require
    multiple files to be rebuilt.

    Faster executables

    Debug builds produce binaries that run considerably faster, and new
    optimizations deliver even better Release build performance.

    Better compiler diagnostics

    Clearer error and warning messages, along with new Fix-its, make it
    easier to write proper Swift 1.2 code.

    Stability improvements

    The most common compiler crashes have been fixed. You should also see
    fewer SourceKit warnings within the Xcode editor.


    这是另一个可以通过类型推断导致大规模减速的案例。合并运算符。

    换行方式:

    1
    abs(some_optional_variable ?? 0)

    1
    abs((some_optional_variable ?? 0) as VARIABLE_TYPE)

    帮助我的编译时间从70年代到13年代


    这对我来说就像魔术一样-加快编译速度。它将编译时间从10分钟缩短为3分钟。

    它说在Other Swift Flags中添加-Onone时,应该打开Whole Module Optimization

    我在Xcode 8.3Xcode 8.2上使用Swift 3


    在Xcode6.3.1中,没有什么对我有用的——当我添加arround 100个swift文件时,Xcode随机挂在build和/或indexing上。我尝试过模块化选项,但没有成功。

    安装和使用Xcode6.4测试版实际上对我很有用。