Implementation of Swift Protocols and Generics

Concept brief

data structure

Swift's data structure can be roughly divided into: Class, Struct, Enum.

Memory Allocation

Each process has an independent process space. The areas in the process space that can be used for memory allocation are mainly divided into two types:

  • Stack area (Stack)
  • Heap _
  • object memory allocation

SILHere we need to use the intermediate language ( Swift Intermediate Language ) generated during the swift compilation process

The Swift programming language is built on LLVM and uses LLVM IR and the LLVM backend to generate code. But the Swift compiler also includes a new high-level intermediate language called SIL. SILHigher-level semantic analysis and optimizations are performed on Swift.

image.png

//main.swift

class Person {

    var name: String?

    func doSometing() { }

}



let person: Person = Person()

person.doSometing()

let type: Person.Type = Person.self
复制代码

Orderswiftc -emit-sil

class_method indicates that the dynamic dispatch of class methods is implemented through sil_vtable.

viewController.swift

class Person {

    var name: String?

    func doSometing() { }

}



class ViewController: UIViewController {



    var person: Person? //在堆区分配内存



    override func viewDidLoad() {

        super.viewDidLoad()

        person = Person()

        person?.doSometing()

    }

}
复制代码

Type Metadata

The Swift runtime keeps a metadata record for every type used in a program, including every instantiation of generic types.

github.com/apple/swift…

The Swift runtime maintains a metadata record for every type used in a program, including every instantiation of a generic type.

In-memory storage structure

CleanShot 2022-03-25 at 18.13.44@2x.png

The first 8 bytes of the object point to the type information, that is, to the metatype. So these 8 bytes are a metatype pointer.

method dispatch method

一个方法会在运行时被调用或者是一个方法被唤起,是因为编译器有一个计算机制,用来选择正确的方法,然后通过传递参数来唤起它,这个机制通常被成为分派(dispatch),分派就是处理方法调用的过程。方法从书写完成到调用完成,概括上会经历编译期和运行期两个阶段,确定哪个方法被执行,也是在这两个时期进行的。故选择正确方法的阶段,可以分为编译期和运行期,而分派机制通过这两个不同的时期分为两种:

  • 静态分派(static dispatch)
  • 动态分派(dynamic dispatch)

能够在编译期确定执行方法的方式叫做静态分派,无法在编译期确定,只能在运行时去确定执行方法的分派方式叫做动态分派。

Static dispatch更快,而且静态分派可以进行内联等进一步的优化,使得执行更快速,性能更高。

但是对于多态的情况,我们不能在编译期确定最终的类型,这里就用到了Dynamic dispatch动态分派。动态分派的实现是,每种类型都会创建一张表,表内是一个包含了方法指针的数组。动态分派更灵活,但是因为有查表和跳转的操作,并且因为很多特点对于编译器来说并不明确,所以相当于block了编译器的一些后期优化。所以速度慢于Static dispatch

bugs.swift.org/browse/SR-5…

组件关系

在swift中组件关系可以分为inheritanceprotocolsgenerics

  • inheritance 继承
  • protocols 协议
  • generics 泛型

多态(特性)

指允许不同对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。

Swift 协议与泛型的实现

问题:

  • Protocol Type 和 Generic Type 如何实现存储?
  • Protocol Type 和 Generic Type 如何拷贝变量?
  • Protocol Type 和 Generic Type 如何进行方法派发?

协议类型 Protocol Type

例子,这里是通过Protocol Type实现多态,几个类之间没有继承关系

protocol Person {

    func doSometing()

}

struct Person1: Person {

    var x, y: Double

    func doSometing() {}

}

struct Person2: Person {

    var x1, y1, x2, y2: Double

    func doSometing() {}

}



//遵守了Person协议的类型集合,可能是Person1或者Person2

var persons: [Person] = [Person1(x: 1.0, y: 1.0), Person2(x1: 1.0, y1: 1.0, x2: 1.0, y2: 1.0)]

for p in persons {

    p.doSometing()

}
复制代码

init_existential_addr %x 初始化由%x 引用的内存为Existential Container。

可以发现在这种情况下,变量 persons 中存储的元素是一种特殊的数据类型:Existential Container

Existential Container

Existential Container 是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协议类型。因为这些数据类型的内存空间尺寸不同,使用 Extential Container 进行管理可以实现存储一致性。

在这里我们可以使用Memlayout这个API去验证一下(本机内存对齐是 8 字节)

可见 Existential Container 类型占用了 5 个内存单元(也称 ,Word)。其结构如下图所示:

CleanShot 2022-03-25 at 16.50.52@2x.png

  • 三个词作为 Value Buffer
  • 一个词作为 Value Witness Table 的索引。
  • 一个词作为 Protocol Witness Table 的索引。

Value Buffer

Value Buffer 占据 3 个词,存储的可能是值,也可能是指针。对于 Small Value(存储空间小于等于 Value Buffer),可以直接内联存储在 Value Buffer 中。对于 Large Value(存储空间大于 Value Buffer),则会在堆区分配内存进行存储,Value Buffer 只存储对应的指针。

Value Witness Table

由于协议类型的具体类型不同,其内存布局也不同,Value Witness Table 则是对协议类型的生命周期进行专项管理,从而处理具体类型的初始化、拷贝、销毁。

Protocol Witness Table

Value Witness Table 管理协议类型的生命周期,Protocol Witness Table 则管理协议类型的方法调用。

在 OOP 中,基于继承关系的多态是通过 Virtual Table 实现的;在 POP 中,没有继承关系,因为无法使用 Virtual Table 实现基于协议的多态,取而代之的是 Protocol Witness Table。

拷贝变量

上面的描述中我们可以了解到

  • 对于 Small Value,直接内联存储在 Existential Container 的 Value Buffer 中。
  • 对于 Large Value,通过堆区分配进行存储,使用 Existential Containter 的 Value Buffer 进行索引。

Indirect Storage With Copy-On-Write

原理:优先使用内存指针,拷贝时仅仅拷贝 Existential Container,当修改值时,先检测引用计数,如果引用计数大于 1,则开辟新的堆区内存。

泛型类型 Generic Type

由Protocol Type实现的多态是动态的多态(Dynamic Polymorphism),那什么是静态多态呢

例子

protocol Person {

    func doSometing()

}

struct Person1: Person {

    var x, y: Double

    func doSometing() {}

}

struct Person2: Person {

    var x1, y1, x2, y2: Double

    func doSometing() {}

}



func todo(person: Person) {

    person.doSometing()

}



let person1 = Person1(x: 1.0, y: 1.0)



todo(person: person1)
复制代码

这个时候通过SIL文件我们可以看到这里还是使用Existential Container数据结构,但是上面我们说得使用Existential Container是为了管理遵守了相同协议的协议类型以及内存对齐,从上面的代码上看这个时候编译器是知道传进去的是Person1类型的对象,故这个时候我们可以使用泛型Generic code来实现。

func todo<T: Person>(person: T) {

    person.doSometing()

}
复制代码

故我们可以根据方法调用时数据类型是否确定可以将多态分为:静态多态(Static Polymorphism)和 动态多态(Dynamic Polymorphism)。

泛型和Protocol Type的区别在于:

  • 泛型支持的是静态多态。
  • 每个调用上下文只有一种类型。
  • 在调用链中会通过类型降级进行类型取代。

Value Buffer

Value Buffer 占据 3 个词,存储的可能是值,也可能是指针。对于 Small Value(存储空间小于等于 Value Buffer),可以直接内联存储在 Value Buffer 中。对于 Large Value(存储空间大于 Value Buffer),则会在堆区分配内存进行存储,Value Buffer 只存储对应的指针。

Value Witness Table

由于协议类型的具体类型不同,其内存布局也不同,Value Witness Table 则是对协议类型的生命周期进行专项管理,从而处理具体类型的初始化、拷贝、销毁。

Protocol Witness Table

Value Witness Table 管理协议类型的生命周期,Protocol Witness Table 则管理协议类型的方法调用。

在 OOP 中,基于继承关系的多态是通过 Virtual Table 实现的;在 POP 中,没有继承关系,因为无法使用 Virtual Table 实现基于协议的多态,取而代之的是 Protocol Witness Table。

拷贝变量

上面的描述中我们可以了解到

  • 对于 Small Value,直接内联存储在 Existential Container 的 Value Buffer 中。
  • 对于 Large Value,通过堆区分配进行存储,使用 Existential Containter 的 Value Buffer 进行索引。

Indirect Storage With Copy-On-Write

原理:优先使用内存指针,拷贝时仅仅拷贝 Existential Container,当修改值时,先检测引用计数,如果引用计数大于 1,则开辟新的堆区内存。

泛型类型 Generic Type

由Protocol Type实现的多态是动态的多态(Dynamic Polymorphism),那什么是静态多态呢

例子

protocol Person {

    func doSometing()

}

struct Person1: Person {

    var x, y: Double

    func doSometing() {}

}

struct Person2: Person {

    var x1, y1, x2, y2: Double

    func doSometing() {}

}



func todo(person: Person) {

    person.doSometing()

}



let person1 = Person1(x: 1.0, y: 1.0)



todo(person: person1)
复制代码

这个时候通过SIL文件我们可以看到这里还是使用Existential Container数据结构,但是上面我们说得使用Existential Container是为了管理遵守了相同协议的协议类型以及内存对齐,从上面的代码上看这个时候编译器是知道传进去的是Person1类型的对象,故这个时候我们可以使用泛型Generic code来实现。

image.png

func todo<T: Person>(person: T) {

    person.doSometing()

}
复制代码

故我们可以根据方法调用时数据类型是否确定可以将多态分为:静态多态(Static Polymorphism)和 动态多态(Dynamic Polymorphism)。

泛型和Protocol Type的区别在于:

  • 泛型支持的是静态多态。
  • 每个调用上下文只有一种类型。
  • 在调用链中会通过类型降级进行类型取代。

方法调用

在方法执行时,Swift将泛型T绑定为调用方使用的具体类型

todo(person: person1) -->todo<T = Person1>(person: person1)

泛型方法调用的具体实现为:

  • 同一种类型的任何实例,都共享同样的实现,即使用同一个Protocol Witness Table。

使用Protocol/Value Witness Table。

  • 每个调用上下文只有一种类型:这里没有使用Existential Container, 而是将Protocol/Value Witness Table作为调用方的额外参数进行传递。
  • Variable initialization and method invocation are both performed using the passed in VWTsum PWT.

generic specialization

After the type is downgraded, a method of a specific type is generated, and a corresponding method is created for each type of the generic type to perform specific optimization under static polymorphism . Virtual function calls are replaced with calling function maps.

E.g

func todo<T: Person>(person: T) {

    person.doSometing()

}

todo(person: person1)

转换成

func todo<Person1>(person: Person1) {

    person.doSometing()

}
复制代码

Because it is static polymorphism. So powerful optimizations can be made, such as inlining the implementation, and further optimizations can be made by getting the context. Thereby reducing the number of methods. It can be more precise and specific after optimization.

References

llvm.org/devmtg/2015…

github.com/apple/swift…

github.com/apple/swift…

www.jianshu.com/p/c2880460c…

www.rightpoint.com/rplabs/swit…

airspeedvelocity.net/2015/03/26/…

developer.apple.com/documentati…

We are the Byte Shenzhen Feishu team, dedicated to creating high-performance, excellent experience enterprise collaborative office software. Interested children's shoes can submit their resumes to [email protected] . iOS Jobs: https://job.toutiao.com/s/Nj3oTKV Android Jobs: https://job.toutiao.com/s/NjTyGkf

Guess you like

Origin juejin.im/post/7078979970436956167