本系列文章将从实践技巧、实现原理以及追踪语言更新等方面对 Swift Protocol 展开深入讨论。主要内容有:
Swift Protocol 背后的故事(实践)
Swift Protocol 背后的故事(理论)
Swift Protocol 背后的故事(Swift 5.6/5.7)
…
本文是系列文章第二篇,主要讨论 Swift Protocol 实现机制。
内容涉及 Type Metadata、Protocol 内存模型 Existential Container、Generics 的实现原理以及泛型特化等。
©原创文章,转载请注明出处!
Type Metadata
在 Objective-C 中,通过 MetaClass 模型来表达类的元信息,并通过实例的 isa
指针来引用 MetaClass。这是整个 Objective-C runtime 的核心机制。
那在 Swift 中类型信息 (Type Metadata) 是如何表达的呢?
Swift runtime 为每种类型 (Class、Struct、Enum、Protocol、Tuple、Function 等等) 生成了一份元信息记录 (Metadata Record),几个关键点:
对于常规类型 (nominal types,如:类、结构体、枚举),其对应的 Metadata Record 在编译期由编译器静态生成;
对于内在类型 (intrinsic types) 以及泛型实例,对应的 Metadata Record 则在运行时动态生成,如:元组、函数、协议等;
每个类型的 Metadata Record 都是唯一的,相同类型的 Metadata Record 是同一个
不同类型的 Metadata 所包含的信息也不一样 (Metadata Layout),但它们有一个共同的头部,其包含:
VWT (Value Witness Table) Poniter — 指向 VWT (虚函数表) 的指针,在 VWT 中包含了该类型的实例如何分配内存 (allocating)、copy (copying)、销毁 (destroying) 等基础操作 (函数指针);
在 VWT 中除了包含上述函数指针外,还有该类型实例的 size、alignment、stride 等基础信息。
Kind — 标记 Metadata 的类型,如:0 – Class,1 – Struct,2 – Enum,12 – Protocol 等等。
与本文讨论相关的是 VWT,关于 Metadata 的更多信息请参考:swift/TypeMetadata.rst at main · apple/swift · GitHub。
Inside Protocol
Existential Container
Class、Struct 以及 Enum 对应的实例都有确定的『模型』用于指导其内存布局。
『模型』就是 Class、Struct 以及 Enum 本身的定义,它们包含的成员。
而 Protocol 并没有确定的『模型』,因为其背后的真实类型可能千奇百怪,那么 Protocol 类型的变量按什么进行内存布局?
Swift 用了一种称之为 Existential Container 的模型来指导 Protocol 变量布局内存。
Existential Container 又分为两类:
Opaque Existential Container — 用于没有类约束的 Protocol (no class constraint on protocol),也就是说这种协议背后的真实类型可能是类、结构体以及枚举等。因此其存储就非常复杂。
1
2
3
4
5struct OpaqueExistentialContainer {
void *fixedSizeBuffer[3];
Metadata *type;
WitnessTable *witnessTables[NUM_WITNESS_TABLES];
};如上,
OpaqueExistentialContainer
包含3个成员:fixedSizeBuffer
— 3 个指针大小的 buffer 空间,当真实类型的 size (内存对齐后的大小) 小于 3 个字时则其内容直接存储在 fixedSizeBuffer 中,否则在 heap 上另辟空间存储,并将指针存储在 fixedSizeBuffer 中;type
— 指向真实类型的 Metadata,最重要的就是引用其中的 VWT 用于完成内存的各种操作;witnessTables
— 指向协议函数表 (Protocol Witness Table, PWT),协议函数表中存储的是真实类型中对应函数的地址。
Class Existential Container — 用于有类约束的 Protocol,该协议背后真实的类型只能是类,而类的实例都是在 Heap 上分配内存的。
因此,在 Existential Container 中只需要一个指向堆内存的指针即可。
同时,由于类的实例含有指向其 Metadata 的指针,故在 Existential Container 中也就没必要再存一份 Metadata 的指针了:
1
2
3
4struct ClassExistentialContainer {
HeapObject *value;
WitnessTable *witnessTables[NUM_WITNESS_TABLES];
};如上,
ClassExistentialContainer
只包含2个成员:value
— 指向堆内存的指针;witnessTables
— PWT 指针。
下面我们来看一个例子:
如图,由于 protocol Drawable
没有 class constraint,故其对应的 Existential Container 是 OpaqueExistentialContainer
:
由于
Point
实例占用 2 个字的内存空间 (小于 3),故对于Drawable
协议类型的变量point
直接使用OpaqueExistentialContainer#buffer
来存储其内容(x
、y
);而
Line
的实例要占用 4 个字的内存空间,故line
需要在 heap 上分配内存用于存储其内容(x0
、y0
、x1
、y1
);Existential Container 中的
type
分别指向了其背后真实类型的 Metadata;PWT 中的函数指针则指向真实类型中的函数。
对应编译器生成的(伪)代码如下:
1 | let point: Drawable = Point(x: 0, y: 0) |
1 | let line: Drawable = Line(x0: 0, y0: 0, x1: 1, y1: 1) |
关于更多 Type Layout 的信息请参考: swift/TypeLayout.rst at main · apple/swift · GitHub
从上面的伪代码可以看到对于协议类型的变量,编译器在背后做了大量的工作。也有一定的性能损耗。
Protocol Type Stored Properties
从上一小节可知,协议类型的变量其实质类型是 Existential Container (OpaqueExistentialContainer
/ ClassExistentialContainer
)。
因此,当协议类型变量作为存储属性时,其在寄主实例中的内存占用就是一个 Existential Container 实例 (下图来自: Understanding Swift Performance · WWDC2016):
小结
Protocol 不同于一般类型 (Class、Struct、Enum),具有以下特点:
使用 Existential Container (
OpaqueExistentialContainer
/ClassExistentialContainer
) 作为内存模型;内存占用 <= 3 的实例,直接存储在 Existential Container buffer 中,否则在 heap 上另行分配内存,并将指针存储在 Existential Container buffer[0] 中;
内存管理 (allocating、copying、destroying) 相关的方法保存在 VWT 中;
通过 PWT 实现方法动态派发 (Dynamic dispatch)。
Generics
泛型作为提升代码灵活性、可复用性的重要手段被大多数语言所支持,如:Swift、Java、C++ (模板)。
Swift 结合 Protocol 赋以泛型更多的灵活性。
下面我们简单探讨一下泛型在 Swift 中是如何实现的。
Swift 中泛型可以添加类型约束 (Type Constraints),约束可以是类,也可以是协议。
因此,Swift 泛型根据类型约束可以分为 3 类:
No Constraints
Class Constraints
Protocol Constraints
下面,分别对这 3 类情况进行简要分析。
通过 SIL (swift/SIL.rst at main · apple/swift · GitHub) 可以大致了解 Swift 背后的实现原理。
swiftc demo.swift -O -emit-sil -o demo-sil.s
如上,通过 swiftc 命令可以生成 SIL。
其中的
-O
是对生成的 SIL 代码进行编译优化,使 SIL 更简洁高效。后面要讲到的泛型特化 (Specialization of Generics) 也只有在
-O
优化下会发生。
No Constraints
其实,这类泛型能执行的操作非常少。无法在 heap 上实例化 (创建) 对象,也不能执行任何方法。
1 | @inline(never) // 禁止编译器做 inline 优化 |
如上,swapTwoValues
用于交换 2 个变量的值,其泛型参数 T
没有任何约束。
其对应的 SIL 如下,关键点:
第
8
行,通过alloc_stack
在栈上为类型T
分配内存 (temp
);第
9
行,通过copy_addr
进行内存级拷贝 (不会执行任何init
方法);第
12
行,通过dealloc_stack
销毁上述开辟的内存;其他就是做一些内存拷贝的操作。
需要注意的是,对于引用类型,
$T
是指针,即在栈上开辟的是存储指针的内存,而非引用类型本身。
1 | 1 // swapTwoValues<A>(_:_:) |
Class Constraints
1 | class Shape { |
如例:
将泛型类型 upcast 到约束类型 (第
5~6
行);通过
class_method
指令找到要执行的方法 (init
、draw
方法 [第7~10
行],在 vtable 中找);可以在 Heap 上创建泛型类型的实例 (第
8
行);其实质就是通过虚函数表实现的多态。
总之,由于有基础类作为类型约束,通过虚函数表就可以执行所有基础类公开的方法。
1 | 1 // drawShape<A>(_:) |
Protocol Constraints
这里讨论的 Protocol 是没有 class constraint 的,对于只能由类实现的协议作为泛型约束时,其效果同上节讨论的 Class Constraints。
1 | @inline(never) |
从下列 SIL 可以看到:
通过
alloc_stack
可以为泛型类型在 Stack 上分配内存 (无论泛型类型是值类型还是引用类型);同样通过
copy_addr
执行内存拷贝;通过
witness_method
指令在泛型类型上查找 Protocol 指定的方法 (查 PWT 表)。
1 | // equal<A>(_:_:) |
再看一个例子:
1 | protocol Drawable { |
从下列 SIL 可以看出:
- 可以在 Heap 上创建泛型类型实例 (无论是值类型还是引用类型);
1 | // drawShape<A>(_:) |
通过上面简单的分析可以看出 No Constraints、Class Constraints 以及 Protocol Constraints 的泛型类型在实现上的区别:
No Constraints 泛型能做的事很少,不能执行任何方法,只能 Stack 上为泛型类型分配内存并执行内存拷贝;
Class Constraints 泛型可以在 Heap 上创建新实例,方法调用通过虚函数表 (vtable) 实现;
Protocol Constraints 泛型可以在 Stack 上也可以在 Heap 上按需创建泛型类型实例,无论泛型是值类型还是引用类型;
Protocol Constraints 泛型通过 PWT 实现方法调用;
也就是说泛型中的方法调用都是动态派发 (Dynamic dispatch),通过 vtable 或者 PWT。
Specialization of Generics
从上一小节可知,泛型方法调用都是动态派发 (通过 vtable 或 PWT),有一定的性能损耗。
为了优化此类损耗,Swift 编译器会对泛型进行特化 (Specialization of Generics)。
所谓特化就是为具体类型生成相应版本的函数,从而将泛型转成非泛型,实现方法调用的静态派发。
1 | @inline(never) |
如例,通过 Int
型参数调用 swapTwoValues
时,编译器就会生成该方法的 Int
版本:
1 | // specialized swapTwoValues<A>(_:_:) |
那么,什么时候会进行泛型特化呢?
总的原则是在编译泛型方法时知道有哪些调用方,同时调用方的类型是可推演的。
最简单的情况就是泛型方法与调用方在同一个源文件里,一起进行编译。
另外在编译时若开启了 Whole-Module Optimization ,同一模块内部的泛型调用也可以被特化。
关于全模块优化请参考Swift.org - Whole-Module Optimization in Swift 3,在此不再赘述。
小结
Swift 为每种类型生成了一份 Metadata Record,其中包含了 VWT (Value Witness Table);
Protocol 使用 Existential Container 作为其内存模型,所有 Protocol 类型的变量都是 Existential Container 的实例;
Protocol 通过 PWT 实现方法动态派发 (Dynamic dispatch);
泛型调用在满足一定条件时会进行特化,以提升性能。
参考资料
swift-evolution · Opaque Result Types
Different flavors of type erasure in Swift
Opaque Return Types and Type Erasure
How to use phantom types in Swift
swift/TypeMetadata.rst at main · apple/swift · GitHub
swift/TypeLayout.rst at main · apple/swift · GitHub