开源中国

我们不支持 IE 10 及以下版本浏览器

It appears you’re using an unsupported browser

为了获得更好的浏览体验,我们强烈建议您使用较新版本的 Chrome、 Firefox、 Safari 等,或者升级到最新版本的IE浏览器。 如果您使用的是 IE 11 或以上版本,请关闭“兼容性视图”。
编写高性能的 Swift 代码 - 技术翻译 - 开源中国社区

编写高性能的 Swift 代码 【已翻译100%】

标签: Swift
oschina 推荐于 2年前 (共 13 段, 翻译完成于 12-16) 评论 11
收藏  
122
推荐标签: Swift 待读

下面这篇文档收集了一系列编写高性能 Swift 代码的要诀和技巧。文档的目标读者是编译器和标准库开发人员。

文档中的一些技巧可以帮助提升您的 Swift 程序质量,使您的代码不容易出错且可读性更好。显式地标记最终类和类协议是两个显而易见的例子。 然而文档中还有一些技巧是不符合规矩的,扭曲的,仅仅解决一些比编译器或语言的特殊的临时性需求。文档中的很多建议来自于多方面的权衡,例如:运行时、字节大小、代码可读性等等。

Sandy_guoguo
 翻译得不错哦!

启用优化

第一个应该做的事情就是启用优化。Swift 提供了三种不同的优化级别:

  • -Onone: 这意味着正常的开发。它执行最小优化和保存所有调试信息。

  • -O: 这意味着对于大多数生产代码。编译器执行积极地优化,可以大大改变提交代码的类型和数量。调试信息将被省略但还是会有损害的。

  • -Ounchecked: 这是一个特殊的优化模式,它意味着特定的库或应用程序,这是以安全性来交换的。编译器将删除所有溢出检查以及一些隐式类型检查。这不是在通常情况下使用的,因为它可能会导致内存安全问题和整数溢出。如果你仔细审查你的代码,那么对整数溢出和类型转换来说是安全的。

在 Xcode UI 中,可以修改的当前优化级别如下: 
...

无若
 翻译得不错哦!

整个组件优化

默认情况下 Swift 单独编译每个文件。这使得 Xcode 可以非常快速的并行编译多个文件。然而,分开编译每个文件可以预防某些编译器优化。Swift 也可以犹如它是一个文件一样编译整个程序,犹如就好像它是一个单一的编译单元一样优化这个程序。这个模式可以使用命令行 flag-whole-module-optimization 来激活。在这种模式下编译的程序将最最有可能需要更长时间来编译,单可以运行得更快。

这个模式可以通过 XCode 构建设置中的“Whole Module Optimization”来激活。

降低动态调度

Swift 在默认情况下是一个类似 Objective-C 的非常动态的语言。与 Objective-C 不同的是,Swift 给了程序员通过消除和减少这种特性来提供运行时性能的能力。本节提供几个可被用于这样的操作的语言结构的例子。

ostatsu
 翻译得不错哦!

动态调度

类使用动态调度的方法和默认的属性访问。因此在下面的代码片段中,a.aProperty、a.doSomething() 和 a.doSomethingElse() 都将通过动态调度来调用:

class A {
  var aProperty: [Int]
  func doSomething() { ... }
  dynamic doSomethingElse() { ... }
}

class B : A {
  override var aProperty {
    get { ... }
    set { ... }
  }

  override func doSomething() { ... }
}

func usingAnA(a: A) {
  a.doSomething()
  a.aProperty = ...
}

在 Swift 中,动态调度默认通过一个 vtable[1](虚函数表)间接调用。如果使用一个 dynamic 关键字来声明,Swift 将会通过调用 Objective-C 通知来发送呼叫代替。这两种情况中,这种情况会比直接的函数调用较慢,因为它防止了对间接呼叫本身之外程序开销的许多编译器优化[2]。在性能关键的代码中,人们常常会想限制这种动态行为。

建议:当你知道声明不需要被重写时使用“final”。

final 关键字是一个类、一个方法、或一个属性声明中的一个限制,使得这样的声明不得被重写。这意味着编译器可以呼叫直接的函数调用代替间接调用。例如下面的 C.array1 和 D.array1 将会被直接[3]访问。与之相反,D.array2 将通过一个虚函数表访问:

final class C {
  // No declarations in class 'C' can be overridden.
  var array1: [Int]
  func doSomething() { ... }
}

class D {
  final var array1 [Int] // 'array1' cannot be overridden by a computed property.
  var array2: [Int]      // 'array2' *can* be overridden by a computed property.
}

func usingC(c: C) {
   c.array1[i] = ... // Can directly access C.array without going through dynamic dispatch.
   c.doSomething() = ... // Can directly call C.doSomething without going through virtual dispatch.
}

func usingD(d: D) {
   d.array1[i] = ... // Can directly access D.array1 without going through dynamic dispatch.
   d.array2[i] = ... // Will access D.array2 through dynamic dispatch.
}
ostatsu
 翻译得不错哦!

建议:当声明的东西不需要被文件外部被访问到的时候,就用“private”

将 private 关键词用在一个声明上,会限制对其进行了声明的文件的可见性。这会让编辑器有能力甄别出所有其它潜在的覆盖声明。如此,由于没有了任何这样的声明,使得编译器可以自动地推断出 final 关键词,并据此去掉对方面的间接调用和属性的访问。例如在如下的 e.doSomething()  和 f.myPrivateVar 中,就将可以被直接访问,假定在同一个文件中,E, F 并没有任何覆盖的声明:

private class E {
  func doSomething() { ... }
}

class F {
  private var myPrivateVar : Int
}

func usingE(e: E) {
  e.doSomething() // There is no sub class in the file that declares this class.
                  // The compiler can remove virtual calls to doSomething()
                  // and directly call A’s doSomething method.
}

func usingF(f: F) -> Int {
  return f.myPrivateVar
}

高效的使用容器类型

通用的容器 Array 和 Dictionary 是有 Swift 标准库提供的一个重要的功能特性。本节将介绍如何用一种高性能的方式使用这些类型。

LeoXu
 翻译得不错哦!

建议:在数组中使用值类型

在 Swift 中,类型可以分为不同的两类:值类型(结构体,枚举,元组)和引用类型(类)。一个关键的区分是 NSArray 不能含有值类型。因此当使用值类型时,优化器就不需要去处理对 NSArray 的支持,从而可以在数组上省去大部分消耗。

此外,相比引用类型,如果值类型递归地含有引用类型,那么值类型仅仅需要引用计数器。而如果使用没有引用类型的值类型,就可以避免额外的开销,从而释放数组内的流量。

// Don't use a class here.
struct PhonebookEntry {
  var name : String
  var number : [Int]
}

var a : [PhonebookEntry]

记住要在使用大值类型和使用引用类型之间做好权衡。在某些情况下,拷贝和移动大值类型数据的消耗要大于移除桥接和持有/释放的消耗。

我是菜鸟我骄傲
 翻译得不错哦!

建议:当 NSArray 桥接不必要时,使用 ContiguousArray 存储引用类型。如果你需要一个引用类型的数组,而且数组不需要桥接到 NSArray 时,使用 ContiguousArray 替代 Array:

class C { ... }
var a: ContiguousArray<C> = [C(...), C(...), ..., C(...)]

建议:使用适当的改变而不是对象分配。

在 Swift 中所有的标准库容器都使用 COW(copy-on-write) 执行拷贝代替即时拷贝。在很多情况下,这可以让编译器通过持有容器而不是深度拷贝,从而省掉不必要的拷贝。如果容器的引用计数大于 1 并容器时被改变时,就会拷贝底层容器。例如:在下面这种情况:当 d 被分配给 c 时不拷贝,但是当 d 经历了结构性的改变追加 2,那么 d 将会被拷贝,然后 2 被追加到 b:

var c: [Int] = [ ... ]
var d = c        // No copy will occur here.
d.append(2)      // A copy *does* occur here.

如果用户不小心时,有时 COW 会引起额外的拷贝。例如,在函数中,试图通过对象分配执行修改。在 Swift 中,所有的参数传递时都会被拷贝一份,例如,参数在调用点之前持有一份,然后在调用的函数结束时释放。也就是说,像下面这样的函数:

func append_one(a: [Int]) -> [Int] {
  a.append(1)
  return a
}

var a = [1, 2, 3]
a = append_one(a)

尽管由于分配,a 的版本没有任何改变 ,在 append_one 后也没有使用 ,  但 a 也许会被拷贝。这可以通过使用 inout 参数来避免这个问题:

func append_one_in_place(inout a: [Int]) {
  a.append(1)
}

var a = [1, 2, 3]
append_one_in_place(&a)
我是菜鸟我骄傲
 翻译得不错哦!

未检查操作

Swift 通过在执行普通计算时检查溢出的方法解决了整数溢出的 bug。这些检查在已确定没有内存安全问题会发生的高效的代码中,是不合适的。

建议:当你确切的知道不会发生溢出时使用未检查整型计算。

在对性能要求高的代码中,如果你知道你的代码是安全的,那么你可以忽略溢出检查。

a : [Int]
b : [Int]
c : [Int]

// Precondition: for all a[i], b[i]: a[i] + b[i] does not overflow!
for i in 0 ... n {
  c[i] = a[i] &+ b[i]
}

泛型

Swift 通过泛型类型的使用,提供了一个非常强大的抽象机制 。Swift 编译器发出一个可以对任何 T 执行 MySwiftFunc<T> 的具体的代码块。生成的代码需要一个函数指针表和一个包含 T 的盒子作为额外的参数。MySwiftFunc<Int> 和 MySwiftFunc<String> 之间的不同的行为通过传递不同的函数指针表和通过盒子提供的抽象大小来说明。一个泛型的例子:

class MySwiftFunc<T> { ... }

MySwiftFunc<Int> X    // Will emit code that works with Int...
MySwiftFunc<String> Y // ... as well as String.

当优化器启用时,Swift 编译器寻找这段代码的调用,并试着确认在调用中具体使用的类型(例如:非泛型类型)。如果泛型函数的定义对优化器来说是可见的,并知道具体类型,Swift 编译器将生成一个有特殊类型的特殊泛型函数。那么调用这个特殊函数的这个过程就可以避免关联泛型的消耗。一些泛型的例子:

class MyStack<T> {
  func push(element: T) { ... }
  func pop() -> T { ... }
}

func myAlgorithm(a: [T], length: Int) { ... }

// The compiler can specialize code of MyStack[Int]
var stackOfInts: MyStack[Int]
// Use stack of ints.
for i in ... {
  stack.push(...)
  stack.pop(...)
}

var arrayOfInts: [Int]
// The compiler can emit a specialized version of 'myAlgorithm' targeted for
// [Int]' types.
myAlgorithm(arrayOfInts, arrayOfInts.length)
我是菜鸟我骄傲
 翻译得不错哦!

建议:将泛型的声明放在使用它的文件中

只有在泛型声明在当前模块可见的情况下优化器才能执行特殊化。这只有在使用泛型的代码和声明泛型的代码在同一个文件中才能发生。注意标准库是一个例外。在标准库中声明的泛型对所有模块可见并可以进行特殊化。

建议:允许编译器进行特殊化

只有当调用位置和被调函数位于同一个编译单元的时候编译器才能对泛型代码进行特殊化。我们可以使用一个技巧让编译器对被调函数进行优化,这个技巧就是在被调函数所在的编译单元中执行类型检查。执行类型检查的代码会重新分发这个调用到泛型函数---可是这一次它携带了类型信息。在下面的代码中,我们在函数 play_a_game 中插入了类型检查,使得代码的速度提高了几百倍。

//Framework.swift:

protocol Pingable { func ping() -> Self }
protocol Playable { func play() }

extension Int : Pingable {
  func ping() -> Int { return self + 1 }
}

class Game<T : Pingable> : Playable {
  var t : T

  init (_ v : T) {t = v}

  func play() {
    for _ in 0...100_000_000 { t = t.ping() }
  }
}

func play_a_game(game : Playable ) {
  // This check allows the optimizer to specialize the
  // generic call 'play'
  if let z = game as? Game<Int> {
    z.play()
  } else {
    game.play()
  }
}

/// -------------- >8

// Application.swift:

play_a_game(Game(10))
萧炎119
 翻译得不错哦!

大的值对象的开销

在 swift 语言中,值类型保存它们数据独有的一份拷贝。使用值类型有很多优点,比如值类型具有独立的状态。当我们拷贝值类型时(相当于复制,初始化参数传递等操作),程序会创建值类型的一个拷贝。对于大的值类型,这种拷贝时很耗费时间的,可能会影响到程序的性能。

让我们看一下下面这段代码。这段代码使用值类型的节点定义了一个树,树的节点包含了协议类型的其他节点,计算机图形场景经常由可以使用值类型表示的实体以及形态变化,因此这个例子很有实践意义

protocol P {}
struct Node : P {
  var left, right : P?
}

struct Tree {
  var node : P?
  init() { ... }
}

当树进行拷贝时(参数传递,初始化或者赋值)整个树都需要被复制.这是一项花销很大的操作,需要很多的 malloc/free 调用以及以及大量的引用计数操作

然而,我们并不关系值是否被拷贝,只要在这些值还在内存中存在就可以。

萧炎119
 翻译得不错哦!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
评论(11)
Ctrl/CMD+Enter

真希望能强到参与Swift编译器和标准库开发的大军中啊!
真希望能强到参与java编译器和标准库开发的大军中啊!
真希望能强到参与java编译器和标准库开发的大军中啊!
真希望能强到参与java编译器和标准库开发的大军中啊!
真希望能强到参与C编译器和标准库开发的大军中啊!
真希望能强到参与java编译器和标准库开发的大军中啊!
真希望能强到参与C#编译器和标准库开发的大军中啊!
真希望能强到参与lol编译器和标准库开发的大军中啊!
楼上的都是 逗B吗
真希望能强到参与C编译器和标准库开发的大军中啊!
顶部