Swift 3 语言中的全模块优化

本文讲的是Swift 3 语言中的全模块优化,


全模块优化是一种 Swift 编译器的优化模式。全模块优化的性能提升很大程度上因项目而异,可达到 2 倍甚至 5 倍的提升。

开启全模块优化可以使用 -whole-module-optimization (或者 -wmo)编译器标识,并且在 Xcode 8 中默认在新项目中被打开。另外 Swift 的包管理器在发布构建中使用全模块优化编译。

那么它是关于什么的?让我们先看看没有全模块优化编译器是如何工作的。

什么是模块和如何编译模块

一个模块是 Swift 文件的集合。每个模块编译成一个独立分布单元-框架(framework)或可执行程序。在单文件编译(没有 -wmo)中,Swift 编译器分别编译模块中的每一个文件。事实上,这就是背后发生的事情。作为一个使用者你不需要手动做这些。编译器驱动或者 Xcode 构建系统会自动完成。

在读取和解析一个源文件(并且完成其他工作,比如类型检查)之后,编译器开始优化 Swift 代码,生成机器码和写目标文件。最终,链接器链接所有目标文件并且生成共享库或者可执行文件。

在单文件编译中编译器的优化仅局限于单个文件。这限制了跨函数优化,比如调用和定义在同一文件中的函数内联或者泛型特殊化。

下面看一个例子。假设我们模块中的一个文件,名为 utils.swift,其中包含一个泛型的实用数据结构体 Container,其中含有一个方法 getElement 并且这个方法在模块中到处被调用,比如在 main.swift 中。

main.swift:

func add (c1: Container, c2: Container) -> Int {
  return c1.getElement() + c2.getElement()
}

utils.swift:

struct Container {
  var element: T

  func getElement() -> T {
    return element
  }
}

当编译器优化 main.swift 时,它并不知道 getElement 如何被实现。它只知道它是存在的。所以编译器生成了一个getElement 的调用。另一个方面,当编译器优化 utils.swift 时,它并不知道函数被调用了哪个具体的类型。所以它只能生成一个通用版本的函数,这比具体类型特殊化过的代码慢很多。

即使简单的在 getElement 中返回声明,都需要在类型的元数据中查找来解决如何拷贝元素。它可以是一个简单的 Int,但它也可以是一个更大的类型,甚至涉及一些引用计数操作。这些编译器都不知道。

全模块优化

拥有全模块优化的编译器可以做的好很多。当使用 -wmo 选项编译时,编译器将模块作为一个整体来优化其中的所有文件。

这么做有两个巨大优势。首先,编译器了解模块中所有函数的实现,所以它能够执行诸如函数内联和函数特殊化等优化。函数特殊化是指编译器创建一个新版本的函数,这个函数通过一个特定的调用上下文来优化性能。例如,编译器能够针对各种具体类型对泛型函数进行特殊化处理。

在我们的例子中,编译器产生了一个使用具体类型 Int 来特殊化泛型 Container 的版本。

struct Container {
  var element: Int

  func getElement() -> Int {
    return element
  }
}

然后编译器可以在 add 函数中内联已经特殊化的 getElement 函数。

func add (c1: Container, c2: Container) -> Int {
  return c1.element + c2.element
}

这个编译仅生成几个机器指令。对比单文件代码有很大的不同,单文件编译中会两次调用 getElement 泛型函数。

跨文件的函数特殊化和函数内联仅是全模块优化的例子。如果编译器了解函数的实现,即使编译器决定不内联一个函数,它也有很大帮助。举例说,它能推出它的引用计数操作的行为。有了这个认识,编译器就能够在函数调用中删除冗余的引用计数操作。

全模块优化的第二大好处是,编译器能够推出所有非公有(non-public)函数的使用。非公有函数仅能在模块内部调用,所以编译器能够确定这些函数的所有引用。那么编译器可以用这个信息做什么?

一个非常基本的优化是消除所谓的「死」函数和方法。这些函数和方法是从未被调用和使用的。使用全模块优化,编译器知道一个非公有函数或方法是否根本没有被使用,如果是这种情况,那么编译器会除去它。为什么程序员会写一个从未被使用的函数?好吧,这不是死函数消除的最重要用例。常用函数变为死函数是其他优化的一个副作用。

我们假设 add 函数只在 Container.getElement 中被调用。在内联 getElement 之后,这个函数不在被使用,所以它可以被删除。即使编译器决定不内联 getElement,编译器也能删除原始 getElement 的泛型版本,因为 add 函数只调用特殊化的版本。

编译时间

单文件编译时,编译器驱动在不同的进程中开始编译每个文件,这能被并行地完成。此外,自从上次编译之后没有被修改的文件就不需要重新编译(假设所有依赖也没有修改)。这被称为增式编译。它节省了大量的编译时间,尤其是当你做了一个小改动的时候。在全模块编译中如何使这个成为可能?让我们来看看全模块优化模式更多的细节。

编译过程有多个阶段:分析程序,类型检查,SIL 优化,LLVM 后端。

大多数情况下,分析程序和类型检查是非常快的,并且我们希望它们在后续的 Swift 发行版中变得更快。SIL 优化程序(SIL 代表 「Swift 中间语言」(Swift Intermediate Language))执行所有 Swift 特定的重要优化,例如泛型特殊化,函数内联等等。编译器的这个阶段通常需要大约三分之一的编译时间。大多数的编译时间花费在 LLVM 后端,它执行更底层的优化和生成代码。

执行全模块优化之后,在 SIL 优化程序中模块被分为多个部分。LLVM 后端使用多线程处理各个部分。如果这个部分自从上次构建以来未被修改,它也会避免重复处理。所以即使使用全模块优化,编译器也能够并行和增量地执行大型编译工作。

结论

全模块优化是一个不用担心如何分配模块中的 Swift 代码也能够得到极大的性能提升的方法,而且。如果上述优化能够在关键代码段执行,性能能够比单文件编译提高 5 倍。并且相比于传统的庞大的全程序优化方法,你能够得到更快的编译时间。





原文发布时间为:2016年11月03日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-11-05 12:08:36

Swift 3 语言中的全模块优化的相关文章

C语言中对于循环结构优化的一些入门级方法简介_C 语言

一.代码移动 将在循环里面多次计算,但是结果不会改变的计算,移到循环外面去. 例子: 优化前: void lower1(char *s){ int i; for(i=0;i<strlen(s);++i) if(s[i]>='A'&&s[i]<='Z') s[i]-=('A'-'a'); } 优化后: void lower2(char *s){ int i; int len=strlen(s); for(int i=0;i<len;++i) if(s[i]>='

c语言-新手OJ, C语言 字符串顺序后移模块,超时,优化无思路 求大神对指点

问题描述 新手OJ, C语言 字符串顺序后移模块,超时,优化无思路 求大神对指点 for(i=1; i<=m; i++){ a=z[n]; for(p=&z[n]; p>=&z[2]; p--)p=(p-1); z[1]=a; } ++++++++++++++++++++++++++++++++++++++++ 作用:将数组z[n]中的所有元素进行向右移m位, 多出来的左端补上. 例如: 12345 m=1 变成 51234 但是我的算法效率太低, 当数组很长, m很大时超时,

深入理解Swift语言中的闭包机制_Swift

在 Swift 中的闭包类似于结构块,并可以在任何地方调用,它就像 C 和 Objective C 语言内置的函数. 函数内部定义的常数和变量引用可被捕获并存储在闭包.函数被视为封闭的特殊情况,它有 3 种形式. 在 Swift 语言闭合表达式,如下优化,重量轻语法风格,其中包括: 推导参数并从上下文菜单返回值的类型 从单封表达的隐性返回 简略参数名称 尾部闭包语法 语法 下面是一个通用的语法定义用于闭包,它接受参数并返回数据的类型: 复制代码 代码如下:   {(parameters) ->

LLVM提议向C语言中加入模块机制

作者 Alex Blewitt ,译者 臧秀涛 发布于 十二月 05, 2012 在今年11月的LLVM开发者大会上,来自Apple的Doug Gregor做了一场讲座,主题是向C语言中加入模块(Module)机制.讲座中提到: 长期以来,C的预处理器就是程序员和工具的问题之源.写得不好的头文件致使宏污染和包含顺序等问题大量存在,程序员必须不断地与之斗争.为了缓解这些问题,开发者习惯上采用各种预处理器变通方案,比如LONG_MACRO_PREFIXES这种风格的很长的宏前缀,#include防卫

Swift语言中如何使用JSON数据教程

原文:Swift语言中如何使用JSON数据教程 这是一篇翻译文章,原文出处:http://www.raywenderlich.com/82706/working-with-json-in-swift-tutorial   Swift语言中如何使用JSON数据教程   JSON(全称:JavaScript Object Notation),是网络服务中传输数据的常用方法,JSON因为容易使用,且可读性强, 所以非常受到欢迎.   下面是个JSON的一个片段: [ {"person": {

Swift语言中的函数学习教程_Swift

函数是一个组织在一起语句集合,以执行特定任务.Swift 函数类似于简单 C 函数以及复杂的 Objective C 语言函数. 它使我们能够通过函数调用内部的局部和全局参数值. 像其他任何语言一样 swift 函数也遵循相同的步骤. 函数声明:它告诉编译器有关的函数的名称,返回类型和参数. 函数定义:它提供函数的实际主体. Swift 函数包含参数类型和返回类型. 函数定义在Swift 语言中函数是由 "func" 关键字来定义.当一个新定义函数时,它可能需要一个或几个值作为函数输入

C语言中的指针和内存泄漏

引言 对于任何使用C语言的人,如果问他们C语言的最大烦恼是什么,其中许多人可能会回答说是指针和内存泄漏.这些的确是消耗了开发人员大多数调试时间的事项.指针和内存泄漏对某些开发人员来说似乎令人畏惧,但是一旦您了解了指针及其关联内存操作的基础,它们就是您在 C 语言中拥有的最强大工具. 本文将与您分享开发人员在开始使用指针来编程前应该知道的秘密.本文内容包括: 导致内存破坏的指针操作类型 在使用动态内存分配时必须考虑的检查点 导致内存泄漏的场景 如果您预先知道什么地方可能出错,那么您就能够小心避免陷

C语言中关键字auto、static、register、const、volatile、extern的作用

原文:C语言中关键字auto.static.register.const.volatile.extern的作用 关键字auto.static.register.const.volatile.extern 这些关键词都是c++基础知识,我整理了一下,希望对新学的朋友们有用: (1)auto 这个这个关键字用于声明变量的生存期为自动,即将不在任何类.结构.枚举.联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量.这个关键字不怎么多写,因为所有的变量默认就是auto的. (2)reg

使用Python标准库中的wave模块绘制乐谱的简单教程_python

在本文中,我们将探讨一种简洁的方式,以此来可视化你的MP3音乐收藏.此方法最终的结果将是一个映射你所有歌曲的正六边形网格地图,其中相似的音轨将处于相邻的位置.不同区域的颜色对应不同的音乐流派(例如:古典.嘻哈.重摇滚).举个例子来说,下面是我所收藏音乐中三张专辑的映射图:Paganini的<Violin Caprices>.Eminem的<The Eminem Show>和Coldplay的<X&Y>. 为了让它更加有趣(在某些情况下更简单),我强加了一些限制.