在Swift 4中优化构建时间

随着Swift的成熟,其编译器也已成熟。 自从Swift 2众所周知的缓慢构建时间以来,我们已经走了很长一段路,再加上当时Xcode的普遍错误,我不确定那时我们是如何完成工作的! 尽管Swift 4和Xcode 9的编译时间似乎变得更好了,我们是否可以进行任何调整以进一步提高性能? 那我们的Swift代码呢? 如果您在过去两年中完全遵循此问题,则可能会遇到有关如何优化Swift代码以加快编译时间的建议。 这些调整是否仍然必要? 在这篇文章中,我将尽力回答这些问题,并从我自己在Swift 4中的构建时间的经验中提供一些学习经验。

快速构建时间-过去,现在和未来

尽管自Swift成立以来就一直观察到构建速度很慢,但直到Matt Matteded发表他关于一个看似简单的代码片段的观察结果,并花了12多个小时才能编译之后,我才觉得它并没有真正成为主流问题。 当然,该错误已在Swift 3中修复,并改进了将Swift和Objective-C代码混合在一起的项目的编译时间。 Swift 4继续改善了编译时间,毫无疑问,当Swift 5明年发布时,我们将看到更多的改进。

缩短Swift构建时间

何时开始做某事

有时很明显,您的编译时间有些不正确。 您可能会注意到,您的项目似乎需要花费很多时间进行编译-尤其是当您经常切换分支时。 在其他情况下,您可能需要花费一些时间来优化构建时间:

  • 项目进行中-在项目中期“健康检查”中尽早发现潜在问题
  • 在项目即将结束时-确保高质量的初始发行版,并为以后的发行版成功做好准备
  • 继承代码库-从长远来看,现在修复速度下降应该会带来收益

缩短Swift构建时间

你能做什么?

您可以采取两项主要措施来缩短构建时间–调整项目设置或调整Swift代码。 调整项目设置很容易,可以带来重大的改进,绝对值得花时间投资。 调整您的Swift代码会花费更多时间,但也会产生积极的结果。

调整项目设置

仅构建活动架构

确保调试构建的仅Build Active Architecture(仅构建活动体系结构 )设置为Yes 。 这是创建新的Xcode项目时的默认设置,但是最好再次检查以确保它不会意外地设置为其他设置。

调试信息格式

确保将“ 调试 选项 ”的“ 构建选项” →“ 调试信息格式”设置为DWARF 。 这是Xcode的默认设置,但可以仔细检查。

注意:如果您使用的是Fabric / Crashlytics,则可能需要使用dSYM File将此设置保持为DWARF 。 当我尝试应用此更改时,我开始从Fabric看到控制台警告,因此必须将其更改回。

使用构建标记启用整个模块优化

此调整涉及为调试构建启用“全模块优化”(WMO),但通过构建标志关闭实际的优化部分。 这样,您仍可以在运行时正确调试代码。

在“ Swift编译器-代码生成”下 ,将“ 调试优化级别”设置为快速的整个模块优化 。 完成此操作后,所有配置都可能最终被设置为此值。

然后,在Swift编译器-自定义标志其他Swift标志下 ,为“ 调试”配置添加“ -Onone”标志。

这允许立即编译整个模块,同时也无需执行任何代码优化。 在许多项目中,我已经看到这种调整将编译时间提高了2倍!

新的Xcode构建系统

Xcode 9引入了一个新的构建系统“预览”,您可以选择加入。 Dan Zinngrabe进行了一些测试,发现对于干净的版本,它的速度提高了28%,对于增量版本,它的速度提高了82%。 我个人在一个项目中看到了6秒钟的加速,对于干净的版本,这提高了19%。 您可能还会注意到Xcode源代码编辑器的响应速度更快,这锦上添花。

要启用新的构建系统,请转到文件项目设置 (或工作区设置 )。

构建系统更改为新的构建系统(预览)

Xcode 9.2并行命令启用功能

随着Xcode 9.2的发布,Apple提供了实验性的选择加入功能,该功能增加了为Swift项目运行的并发构建任务的数量。 苹果公司指出,这可能会减少构建时间,但会消耗内存。

要选择加入,请在终端中运行以下命令:

 默认写com.apple.dt.Xcode BuildSystemScheduleInherentlyParallelCommandsExclusively -bool否 

要退出,请删除首选项:

 默认删除com.apple.dt.Xcode BuildSystemScheduleInherentlyParallelCommandsExclusively 

值得注意的是,在我的测试中,启用该功能后,我发现构建时间没有差异,但是您的里程可能会有所不同。

减少依赖

优化的结果在各个项目中会有所不同,但是根据一般经验,最好将项目依赖性降到最低。 想法是更少的代码要编译==更快的编译时间。

Swift的新功能还可以消除对某些通用库的需求。 Swift 4看到了Codable的发布,从而消除了对第三方JSON解析/序列化库的需求。 明年,Swift 5可能会引入本机并发机制,从而消除对第三方Promise / Future / Operation异步库的需求。

生成时间慢的原因

根据Robert Gummesson的说法,Swift构建时间缓慢的主要原因有两个:

  • 单个例程的编译时间太长-为了加快处理速度,您可以尝试以更快的编译速度来重写代码
  • 闭包和惰性属性进行类型检查的次数过多,这似乎在Swift 4中几乎没有问题-在测试中,我从未见过任何代码多次进行类型检查。

调整您的Swift代码

如何知道要调整什么

在项目中识别可能需要很长时间才能编译的“问题代码”的最简单方法是使用Swift BuildTimeAnalyzer工具。 按照安装说明进行操作,以允许该工具对项目中的代码进行概要分析,然后执行干净的构建。 构建完成后,您可以确切看到每个函数编译所需的时间。

常见的Swift“问题代码”

在过去的几年中,许多人公开了他们的观点,即什么样的Swift代码似乎给编译器带来了麻烦。 幸运的是,其中很多似乎在Swift 4中似乎不再适用(至少从我自己的观察来看)。 如果您对过去导致速度下降的原因感到好奇,请随时查看我的要点或文章结尾的“资源”部分。

我在Swift 4中的“问题代码”观察

尽管过去似乎困扰着Swift的许多“问题代码”似乎不再是一个问题,但我确实注意到了一些共同的主题,这些主题似乎在Swift 4中造成了一些细微的放慢。

守卫和致命错误

这涉及到一个没有提供太多价值的额外guard声明。 在优化之前,此方法花费了182ms的时间进行编译:

  func item(forIndexPath indexPath:IndexPath)-> SomeCellModel { 
 让itemIndex = indexPath.row +(page * pageSize) 
 后卫itemIndex <cellModels.count else { 
fatalError(“索引超出范围”)
}
 返回cellModels [itemIndex] 
  } 

由于系统在超出范围访问数组索引时将产生fatalError ,因此不需要额外的guard导致手动定义的fatalError 。 删除无关的guard ,该方法花费了91ms的时间进行编译:

  func item(forIndexPath indexPath:IndexPath)-> SomeCellModel { 
 让itemIndex = indexPath.row +(page * pageSize) 
 返回cellModels [itemIndex] 
  } 

复杂方法

我看到了几个实例,这些实例在执行多项工作时需要花费很长时间进行编译。 一个常见的主题是直接在viewDidLoad()进行很多设置和配置。 在以下示例中, viewDidLoad()花费了135ms进行编译:

  //标记:-生命周期 
 覆盖func viewDidLoad(){ 
  super.viewDidLoad() 
 警卫队让detailState = detailState else {fatalError(“ \(DetailState.self)未提供给\(self)”)} 
 标题= detailState.navTitle 
  navigationController?.navigationBar.titleTextAttributes = [.font:UIFont.systemFont(ofSize:20)] 
  navigationController?.navigationBar.tintColor = .lightGray 
  validationController = SomeValidationController(fieldProvider:self,detailState:detailState) 
  validationView.delegate =自我 
  setupRadioButtonView() 
  } 
  // MARK:-私人 
 私人功能setupRadioButtonView(){ 
 守卫let detailState = detailState else {return} 
  radioButtonView.configure(withButtonConfig:detailState.radioButtonConfig) 
  radioButtonView.delegate =自我 
  } 

还要注意的是,上面的setupRadioButtonView()方法没有出现在BuildTimeAnalyzer中,这意味着编译所需的时间少于10毫秒

在执行了对viewDidLoad()一些清理以更好地遵循单一职责原则之后,该方法花费了106ms的时间进行编译。

  //标记:-生命周期 
 覆盖func viewDidLoad(){ 
  super.viewDidLoad() 
 警卫队让detailState = detailState else {fatalError(“ \(DetailState.self)未提供给\(self)”)} 
  setupNavigationBar(withState:detailState) 
  setupValidationController(withState:detailState) 
  setupRadioButtonView(withState:detailState) 
  } 
  // MARK:-私人 
 私人功能setupNavigationBar(withState detailState:DetailState){ 
 标题= detailState.navTitle 
  navigationController?.navigationBar.titleTextAttributes = [.font:UIFont.systemFont(ofSize:20)] 
  navigationController?.navigationBar.tintColor = .lightGray 
  } 
 私人功能setupValidationController(withState detailState:DetailState){ 
  validationController = SomeValidationController(fieldProvider:self,detailState:detailState) 
  validationView.delegate =自我 
  } 
 私人功能setupRadioButtonView(withState detailState:DetailState){ 
  radioButtonView.configure(withButtonConfig:detailState.radioButtonConfig) 
  radioButtonView.delegate =自我 
  } 

值得注意的是,还创建了两个私有方法的编译时间。 setupNavigationBar(_:)方法花费了29ms的时间进行编译,而setupValidationController(_:)方法没有在BuildTimeAnalyzer上注册,因此也花费了不到10ms的时间进行编译。 那么,是否通过将viewDidLoad()拆分为这些方法而最终节省了任何时间呢? 如果将这三种方法的时间加起来,则看起来像这样:

  106ms + 29ms + <10ms = 136-144ms 

与原始的viewDidLoad()135ms编译时间相比,您可能会认为我们实际上正在放慢速度。 但是,请务必记住,函数的编译时间通常在BuildTimeAnalyzer运行之间的10–20ms之间变化。 尽管所有内容的编译速度可能都较慢,但事实是,将方法拆分后的编译时间至少要比以前快。 您还可以获得额外的好处,即代码更具可读性,并且更好地坚持了关注点分离。

字符串插值

当我使用BuildTimeAnalyzer分析代码时,发现有趣的一件事是,字符串连接似乎不再是Swift 4编译器难以解决的问题。 当我第一次遇到下面的代码片段时,我以为自己遇到了麻烦,因为我不认为有一种方法可以使代码编译更快:

  struct StorageConfig { 
 让storageName:字符串 
 让installationId:字符串 
  var storageIdentifier:字符串{ 
 返回“ \(存储名称)_ \(安装ID)” 
  } 
  } 

上面的storageIdentifier属性需要127ms的时间进行编译。 只是为了踢球,我决定将String插值替换为String串联。 这使属性可以在60ms内编译:

  struct StorageConfig { 
 让storageName:字符串 
 让installationId:字符串 
  var storageIdentifier:字符串{ 
 返回storageName +“ _” + installationId 
  } 
  } 

我不确定我的发现在所有情况下是否都适用,但是如果您发现要减少使用String插值的某些代码的编译时间,请尝试使用String串联。

手动优化Swift代码-经验教训

Swift 4编译器似乎在减少编译速度瓶颈方面向前迈出了一大步。 这是我所学到的:

  • 不必为了更快的编译时间而牺牲Swift的语法糖和简洁的编码样式
  • 尝试使用字符串插值和字符串串联-插值可能并不总是更快
  • Swift 4编译器似乎只进行类型检查一次,效率更高
  • 高编译时间可能表示关注点分离不良。 在可能的情况下,以此为指导分解复杂功能

您应该手动优化您的Swift代码吗?

我已经向您展示了如何,但是您是否应该花时间手动优化/调整您的Swift代码以加快编译时间? 总的来说,我认为通过缩短编译时间所节省的时间不值得花费时间来调整或“修复”缓慢编译的代码。 但是,如上所述,花时间确定问题区域可能是改进代码遵守单一职责原则和关注点分离的良好方式的好方法。

此外,Swift会随着时间的推移不断改进。 这意味着随着时间的流逝,当前解决方法的收益将逐渐减少。 从长远来看,牺牲简洁性和Swift语法糖现在可能不会有任何好处。

在我的测试中,修复了在我的一个项目中缓慢编译的23个“问题代码”实例之后,我仅看到大约2秒的全新构建速度。 如果您想知道,我认为超过130ms的东西是我需要修复的东西(尽管一开始编译不需要花费200ms的时间)。 这是针对一个未完成的项目,该项目包含约10,000行非注释,非空白的Swift 4代码。

您应该关心Swift编译时间吗?

如果我不建议优化您的Swift代码以缩短编译时间,您是否还要关心项目中的编译时间? 简短的答案是肯定的 ! 至少采取一些步骤来缩短项目的编译时间有很多好处:

  • 花费更少的时间来编写代码,而花费更多的时间来编写代码
  • 更快的编译时间使保持认知重点和“流程”更加容易
  • 支持您(包括您自己)的开发人员将感谢您不要让事情失控

全部放在一起

您应该采取哪些步骤来优化Swift 4的构建时间? 这是我将使您的项目构建更快一些的步骤。

建立基准

运行Swift BuildTimeAnalyzer来检查项目的当前状态。 确保没有明显的问题。 就我个人而言,我会考虑花费超过250–500ms的任何代码来编译应在执行更多优化之前固定的内容。 当然,这将取决于项目的大小和计算机的处理能力。 在我的一个项目中,我看到一个文件要花11 秒钟以上才能被编译。 有问题的代码涉及JSON解码库Argo,无论如何我都打算替换它,因为Swift 4的Codable是更好的解决方案。

手动调整代码(可选)

我将由您决定是否认为有必要对代码进行些微调整以减少编译所需的时间。 至少,您可能希望使用数据来改善您的功能遵守单一职责原则的程度。

调整项目设置

如果没有其他要求, 请执行此操作 ! 只需几分钟即可确保优化项目设置,并且通常可以将干净的构建时间减少一半。

另外,尝试一下Xcode 9中的新构建系统。 尽管仍处于“预览”状态,但它可能会缩短您的构建时间以及您的整体Xcode源代码编辑器体验。

减少依赖

通常,将依赖关系降到最低是有益的,这样当发现错误或发布新版本的Xcode / Swift时,您不必依赖其他人来修复/更新代码。 对Swift的改进还可以消除对第三方库的需求,例如Codable的出现取代了对第三方JSON解析库的需求。

面向未来的项目

确保在项目成熟时密切注意编译时间。 与其记住经常运行BuildTimeAnalyzer,不如考虑修改一些最终项目设置以在代码需要很长时间编译时生成警告。

在项目的构建设置中的“ Swift编译器—自定义标志其他Swift标志”下 ,添加以下两个标志:

  • -Xfrontend -warn-long-function-bodies=200
  • -Xfrontend -warn-long-expression-type-checking=200

第一个标志- warn-long-function-bodies -已经存在了一段时间,并将报告所有花费时间超过类型检查阈值的功能 。 Xcode 9中最近引入了第二个标志warn-long-expression-type-checking该标志将报告花费时间超过类型检查阈值的所有表达式

在您认为适合项目和构建系统时,可以随意调整警告阈值。 在上面的示例中,耗时超过200毫秒的所有内容都会在您构建项目时吐出编译器警告。

如果启用“整体模块优化”调整,由于所有内容的编译速度都会更快,因此您可能还希望大幅降低该值。 我建议启用WMO调整后,以100ms的阈值开始。

结论

希望您发现这里的信息有用。 总体而言,就Swift编译时间而言,情况似乎会好得多,但仍有一些小调整可以大大提高项目的编译时间。 我花了几个小时研究这个主题并收集信息,并在以下部分列出了我的资源。 请随时查看它们,并让我知道是否有任何我想念的东西,或者您有自己的提示和技巧来加快处理速度。 就个人而言,我能够在一个项目中将构建时间从61s减少到26s (加速57%),在另一个项目中将构建时间从64s减少23s (64%加速)。 最后,我希望您花几分钟来优化您的项目,以便您可以轻松完成! 😉

资源资源

  • 分析您的Swift编译时间
  • 回家Swift编译器,你醉了
  • 关于Swift构建时间优化
  • 快速的构建时间优化-第2部分
  • #235:使用BuildTimeAnalyzer调试慢速构建
  • 改善Swift编译时间
  • 探索Swift的构建时间
  • 加快Swift编译时间
  • 如何使Swift编译更快
  • 缩短Swift编译时间
  • 在Xcode 9中测量Swift编译时间
  • 优化Swift的构建时间
  • 为您的Swift项目获得更快,更稳定的Xcode
  • 加快Swift项目的编译时间