作者 原文链接 版本
apple/swift Swift Compiler Performance 5dcc32f

本文档是一份关于理解、诊断并报告 Swift 编译器编译性能问题的指南。即:编译器编译代码的速度,而非代码运行的速度。

虽然本指南较长,但内容其实相当简单。在很大程度上,性能分析需要耐住性子、考虑周全且坚持不懈,谨小慎微且始终如一地测量,并逐步消除噪音且专注于一个信号。

影响编译性能的流程与因素概述

本节从较为宏观的角度论述关于编译器在运行时所做的工作,不仅包括显而易见的「编译」,以及影响编译器耗时的主要因素。

当我们使用 Xcode 或在命令行中编译或运行 Swift 程序时,通常将调用 swiftswiftc(后者是前者的符号链接),这是一个根据不同参数能够以显著不同的方式运行的程序。

虽然它可以直接编译或运行代码,但它通常会反过来运行一个或多个 swiftswiftc 副本作为子进程。在典型的批量编译中,swiftc 的第一个副本将被作为**驱动(Driver)进程运行,之后它将在进程树中运行一些所谓的前端(Frontend)**子进程。当我们要理解 Swift 编译时,我们必须清楚地了解哪些进程在运行,以及它们在做什么:

  • 驱动:子进程树中的顶层 swiftc 进程。负责决定哪些文件需要被编译或重新编译,以及运行子进程。子进程即所谓的作业(Jobs),它们运行编译和链接步骤。通常在运行时,驱动是空闲的,等待其它子进程完成。
  • 前端作业(Frontend Jobs):由驱动启动的子进程,运行 swift -frontend ...、运行编译、PCH 文件生成、模块合并等。这些作业大量增加了编译开销。
  • 其它作业(Other Jobs):由驱动启动的子进程,运行 ldswift -modulewrapswift-autolink-extractdsymutildwarfdump,以及涉及收尾前端已完成作业等类似工具。其中一些也是 swift 程序,但它们并非「运行前端作业」,因此将会有完全不同的配置。

这些运行的一系列作业以及它们的耗时方式高度依赖于编译模式。有关编译性能的这些模式信息将在下一节中详述;关于驱动的更多详细信息,请参阅驱动文档、以及有关驱动内部驱动可解析输出的文档。

在下一节讨论完编译模式之后,我们还将触及可能在没有明显热点出现的情况下,所发生的工作负载大幅变化,这将分别从惰性策略(Laziness Strategies)与近似两个角度论述。

编译模式

编译器有许多不同的选项可以控制驱动和前端作业,但导致行为上最显著差异的两个维度通常被称作模式(Modes)。因此当我们着眼于编译时,弄清楚 swiftc 的运行模式便十分重要,以及要时常对每个模式进行独立的分析。这些重要的模式如下:

  • 主文件(Primary-File)整模块(Whole-Module):不同模式取决于驱动是否使用 -wmo(又名 -whole-module-optimization)参数运行。
    • **批量(Batch)单文件(Single-File)**主文件模式:随着 Swift 4.2 正式版中加入了新的批量模式,这种区分完善了主文件模式的行为。批量模式减小了主文件模式的众多开销,并最终成为运行主文件模式的默认方式,但在此之前开启批量模式需传递 -enable-batch-mode 参数。
  • 优化(Optimizing)无优化(Non-Optimizing):不同模式取决于驱动(以及因此每个前端)是否使用 -O-Osize-Ounchecked(每个参数代表开启一个或多个优化选项),或默认(无优化),即等同于 -Onone-Oplayground

当我们使用 Xcode 或用 xcodebuild 构建程序时,通常有一个配置参数会同时切换这两种模式。也就是说,典型的代码有两种配置:

  • Debug:即结合主文件模式与 -Onone
  • Release:即结合 WMO 模式与 -O

这些参数均可单独更改,编译器的耗时也将根据设置而有所不同,因此这值得我们更详细地了解这两个维度。

主文件(带与不带批处理)与 WMO

这是编译器行为中最重要的变量,因此这值得我们彻底搞清楚:

  • 主文件模式下,驱动将它要运行的工作分配给多个前端进程,每个前端完成后将得出部分结果,驱动在所有前端完成后合并这些结果。每个前端作业本身读取模块中的全部文件,专注于其编译时读取部分中的一个或多个主要文件,并根据需要从模块中惰性分析其它引用定义。该模式有两种子模式:
    • 单文件子模式下,编译器对每个文件运行一个前端作业,且每个作业只有一个主文件。
    • 批处理子模式下,编译器对每个 CPU 运行一个前端作业,将模块文件中等大的「批处理」标示为主文件。
  • 整模块(WMO)模式下,驱动只为整个模块运行一个前端作业。该前端一次性读取模块中的全部文件,并一次性全部编译。

举个例子:如果我们有一个包含 100 个文件的模块:

  • 运行 swiftc *.swift 将以单文件模式编译,因此将运行 100 个前端子进程,每个子进程将解析全部 100 个输入(总共 10,000 次解析),最后每个子进程将(并行)编译单个主文件的定义。
  • 运行 swiftc -enable-batch-mode *.swift 将以批处理模式编译,因此在一个拥有 4 个 CPU 的系统上将运行 4 个前端子进程,每个子进程将解析全部 100 个输入(总共 400 次解析),最后每个子进程将(并行)编译 25 个主文件定义(每个进程中模块数的四分之一)。
  • 运行 swiftc -wmo *.swift 将以整模块模式编译,因此将运行一个前端子进程,并一次性读取全部 100 个文件(总共 100 次解析),最后按顺序(串行)编译全部文件的定义。

为什么存在多种模式?因为它们各自有不同的优点与不足;没有一种模式是完美的:

  • 主文件模式的优点是,驱动可以通过仅为已过时的文件运行前端从而进行增量编译(Incremental Compilation),以及利用多核并行运行多个前端作业。其不足是每个前端作业必须在专注于其感兴趣的主文件之前读取模块中的全部源文件,这意味着前端作业中的一部分将被完成两次

大量优化

工作负载

增量编译

惰性决议

总结

已知问题区域

如何诊断编译性能问题

工具与选项

分析器

Instruments.app
Perf

诊断选项

统一状态报告器
跟踪状态事件

用于诊断的后处理工具

人造分析工具

缩小器

Git
Creduce
通用

独立回归

驱动诊断

寻找需要通用提升的区域

编译器计数器

规模测试

如何最有用地报告 Bug

如何