理解 Swift 中的方法派发机制 – 静态派发

办法派发

在计算机领域中,有两种类型的办法派发办法,并且它们有着明显的差异:

  • 静态派发(Static dispatch):速度快不灵敏。
  • 动态派发(Dynamic dispatch):速度慢但愈加灵敏。

这两大类还能根据速度与灵敏性的不同,细分成下面的四小类:

  • 内联办法:速度最快,灵敏性最差。
  • 静态派发
  • 表派发
  • 音讯派发:速度最慢,灵敏性最好。

这种层次结构是由直接层次决议的。浅显地说,这意味着“找到并履行一个函数所需的跳转次数”:

  • 内联办法:无需跳转。
  • 静态派发:只需跳转一次即可找到履行函数。
  • 表派发:需要跳转两次,一次跳转到函数指针表,一次是跳转到函数本身。
  • 音讯派发:根据代码的数据结构或许会跳转很屡次。

大多数言语仅支撑上述的某几种办法派发,而 Swift 则支撑上述所有的办法派发办法。这是一把双刃剑:它使开发者能够对其代码的功能特征进行细粒度的操控;但假如运用不当,也会导致许多问题。

静态派发

内联办法

这是最快的办法派发机制,实际上它也算不上是办法派发。内联是一种编译器优化,它实际上用函数中的代码替换函数的调用点。

一般来说,咱们无法操控这一点:Swift 编译器在其优化阶段做出有关内联函数调用的决议。

代码示例如下:

func addOne(to num: Int) -> Int {
    return num + 1
}
let twoPlusOne = addOne(to: 2)

假如编译器决议内联它,编译后的 Swift 或许相当于这样:

let twoPlusOne = 2 + 1

由于上面示例运用硬编码数字,因而编译器实际上拥有在编译时计算 addOne 成果所需的所有信息。这意味着编译器能够履行进一步的优化:返回值能够预先计算,优化完代码如下:

let twoPlusOne = 3

估计算是最终的优化,由于咱们此刻乃至没有履行代码。也便是说,咱们的函数调用的成果在编译时就已知,因而在运转此段代码时,用户的设备实际上不需要进行任何工作。

Swift Intermediate Language

在将代码编译为机器言语之前,Swift 编译器会将其转化为 Swift 中心言语 (SIL),并在其间运转许多优化过程。

下面这些奥秘的象形文字让咱们能够亲眼看到优化:

sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
  // 1
  alloc_global @$s4main10twoPlusOneSivp 
  // 2  
  %3 = global_addr @$s4main10twoPlusOneSivp 
  // 3 
  %4 = integer_literal $Builtin.Int64, 3
  // 4
  %5 = struct $Int (%4 : $Builtin.Int64)
  // 5 
  store %5 to %3 : $*Int
  // 6
  %7 = integer_literal $Builtin.Int32, 0          
  %8 = struct $Int32 (%7 : $Builtin.Int32)        
  return %8 : $Int32                              

为了简洁起见,我省略了大部分代码,但咱们能够看到内联的实际效果:

  • 内存被分配给 twoPlusOne 特点。
  • 分配 twoPlusOne 特点的指针地址。
  • 这便是奇特的当地:3 的整数文字被预先计算并内联,彻底避免了办法调用。
  • 该值从标准库转化为 Int 结构体。
  • Int 存储%3 处,即 twoPlusOne 的内存地址。
  • 一般会在 main() 函数的末尾看到这些行, 这只是以代码 0 退出程序。

假如你想亲自查看 SIL,能够运用命令 swiftc -emit-sil -O main.swift > sil.txt 进行转化。

-O 告诉编译器运转速度优化,其间包括内联。 -Osize 相反使编译器不太或许内联代码,由于在多个位置内联函数会添加二进制巨细。

静态派发

Swift 中的静态函数以及枚举和结构上的函数始终运用静态派发。当 Swift 程序运转时,这些函数编译后的机器代码存储在内存中的已知地址处。

静态派发的这种确定性使得编译器能够运转内联和估计算等优化。

比如下面的示例代码:

struct Adder {
    func addOne(to int: Int) -> Int {
        return int + 1
    }
}
let threePlusOne = Adder().addOne(to: 3)

让咱们为这段代码生成 Swift 中心言语,看看编译器的底层发生了什么:

// 此代码为简化后的代码
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
  // 1
  alloc_global @$s4main12threePlusOneSivp   
  %3 = global_addr @$s4main12threePlusOneSivp : $*Int
  // 2 
  %4 = metatype $@thin Adder.Type
  %5 = function_ref @$s4main5AdderVACycfC : $@convention(method) (@thin Adder.Type) -> Adder 
  %6 = apply %5(%4) : $@convention(method) (@thin Adder.Type) -> Adder 
  // 3
  %7 = integer_literal $Builtin.Int64, 3          
  %8 = struct $Int (%7 : $Builtin.Int64)          
  // 4
  %9 = function_ref @$s4main5AdderV6addOne2toS2i_tF : $@convention(method) (Int, Adder) -> Int 
  %10 = apply %9(%8, %6) : $@convention(method) (Int, Adder) -> Int
  store %10 to %3 : $*Int   
  • ThreePlusOne 特点分配内存。
  • Adder 结构的 init 函数被调用。 apply 是用于调用函数的 SIL 指令,将 %4(类型)作为 %5(函数)的参数。
  • 接下来,函数参数的整数字面量即数字 3 被实例化。首要调用 Builtin Literal,然后初始化 Int
  • 最终,addOne 函数被调用;创建函数指针 function_ref,并传递之前创建的参数:IntAdder

SIL 的调用与 Python 的调用非常相似,其间 self(实例)显式传递到其办法的调用站点。这是由于类型上的办法在内存中的所有实例之间同享。因而,需要对实例的引用才干拜访或更改任何特点。

Swift 编译器内联地折叠整个静态派发函数调用链,以一次性消除许多贵重的函数调用。这便是静态派发速度快的原因。