办法派发
在计算机领域中,有两种类型的办法派发办法,并且它们有着明显的差异:
- 静态派发(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
,并传递之前创建的参数:Int
和Adder
。
SIL 的调用与 Python 的调用非常相似,其间 self
(实例)显式传递到其办法的调用站点。这是由于类型上的办法在内存中的所有实例之间同享。因而,需要对实例的引用才干拜访或更改任何特点。
Swift 编译器内联地折叠整个静态派发函数调用链,以一次性消除许多贵重的函数调用。这便是静态派发速度快的原因。