起初的疑问源自于「在 Swift 中的, Struct:Protocol 比 抽象类 好在哪里?」。可是找来找去都是 Swift 性能相关的东西。整理了点笔记,供你们能够参考一下。git
在正题开始以前,不知道你是否有以下的疑问:程序员
若是你也有相似疑问,但愿这篇笔记能帮你解释一下上面几个问题的一些缘由。(ps.上面几个问题都很大,若是有不一样的想法和了解,也但愿你能分享出来,你们一块儿讨论一下。)github
首先,咱们先统一一下关于类型的几个概念。算法
有些类型只须要按照字节表示进行操做,而不须要额外工做,咱们将这种类型叫作平凡类型 (trivial)。好比,Int 和 Float 就是平凡类型,那些只包含平凡值的 struct 或者 enum 也是平凡类型。编程
struct AStruct {
var a: Int
}
struct BStruct {
var a: AStruct
}
// AStruct & BStruct 都是平凡类型
复制代码
对于引用类型,值实例是一个对某个对象的引用。复制这个值实例意味着建立一个新的引用,这将使引用计数增长。销毁这个值实例意味着销毁一个引用,这会使引用计数减小。不断减小引用计数,最后固然它会变成 0,并致使对象被销毁。可是须要特别注意的是,咱们这里谈到的复制和销毁值,只是对引用计数的操做,而不是复制或者销毁对象自己。swift
struct CStruct {
var a: Int
}
class AClass {
var a: CStruct
}
class BClass {
var a: AClass
}
// AClass & BClass 都是引用类型
复制代码
相似 AClass 这类,引用类型包含平凡类型的,其实仍是引用类型,可是对于平凡类型包含引用类型,咱们暂且称之为组合类型。数组
struct DStruct {
var a: AClass
}
// DStruct 是组合类型
复制代码
主要缘由在下面几个方面:安全
今天主要谈一谈 内存分区 中的 堆 和 栈。性能优化
堆是用于存放进程运行中被动态分配的内存段
,它的大小并不固定,可动态扩张或 缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张); 当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)数据结构
栈又称堆栈, 是用户存放程序临时建立的局部变量
,也就是说咱们函数括弧“{}” 中定义的变量(但不包括static声明的变量,static意味着在数据段
中存放变量)。除此之外, 在函数被调用时,其参数也会被压入发起调用的进程栈中,而且待到调用结束后,函数的返回值 也会被存放回栈中。因为栈的后进先出特色,因此 栈特别方便用来保存/恢复调用现场。从这个意义上讲,咱们能够把堆栈当作一个寄存、交换临时数据的内存区。
在 Swift 中,对于 平凡类型 来讲都是存在 栈 中的,而 引用类型 则是存在于 堆 中的,以下图所示:
咱们都知道,Swift建议咱们多用 平凡类型,那么 平凡类型 比 引用类型 好在哪呢?换句话说「在 栈 中的数据和 堆 中的数据相比有什么优点?」
综上几点,在内存分配的时候,尽量选择 栈 而不是 堆 会让程序运行起来更加快。
首先 引用计数 是一种 内存管理技术,不须要程序员直接去操做指针来管理内存。
而采用 引用计数 的 内存管理技术,会带来一些性能上的影响。主要如下两个方面:
对于 自动引用计数 来讲, 在添加 release/retain 的时候采用的是一个宁肯多写也不漏写的原则,因此 release/retain 有必定的冗余。这个冗余量大概在 10% 的左右(以下图,图片来自于iOS可执行文件瘦身方法)。
而这也是为何虽然 ARC 底层对于内存管理的算法进行了优化,在速度上也并无比 MRC 写出来的快的缘由。这篇文章 详细描述了 ARC 和 MRC 在速度上的比较。
综上,虽然由于自动引用计数的引入,大大减小了内存管理相关的事情,可是对于引用计数来讲,过多或者冗余的引用计数是会减慢程序的运行的。
而对于引用计数来讲,还有一个权衡问题,具体如何权衡会再后文解释。
在 Swift 中, 方法的调度主要分为两种:
struct Point {
var x, y: Double
func draw() {
// Point.draw implementation
}
}
func drawAPoint(_ param: Point) {
param.draw()
}
let point = Point(x: 0, y: 0)
drawAPoint(point)
// 1.编译后变为下面的inline方式
point.draw()
// 2.运行时,直接跳到实现 Point.draw implementation
复制代码
所以,在性能上「静态调度 > 动态调度」而且「Swift中的V-Table > Objective-C 的动态调度」。
在 Swift 引入了一个 协议类型 的概念,示例以下:
protocol Drawable {
func draw()
}
struct Point : Drawable {
var x, y: Double
func draw() { ... }
}
struct Line : Drawable {
var x1, y1, x2, y2: Double
func draw() { ... }
}
var drawables: [Drawable]
// Drawable 就称为协议类型
for d in drawables {
d.draw()
}
复制代码
在上述代码中,Drawable 就称为协议类型,因为 平凡类型 没有继承,因此实现多态上出现了一些棘手的问题,可是 Swift 引入了 协议类型 很好的解决了 平凡类型 多态的问题,可是在设计 协议类型 的时候有两个最主要的问题:
对于第一个问题,如何去调度一个方法?由于对于 平凡类型 来讲,并无什么虚函数指针,因此在 Swift 中并无 V-Table 的方式,可是仍是用到了一个叫作 The Protocol Witness Table (PWT) 的函数表,以下图所示:
对于每个 Struct:Protocol 都会生成一个 StructProtocol 的 PWT。
对于第二个问题,如何保证内存对齐问题?
有一个简单粗暴的方式就是,取最大的Size做为数组的内存对齐的标准,可是这样一来不但会形成内存浪费的问题,还会有一个更棘手的问题,如何去寻找最大的Size。因此为了解决这个问题,Swift 引入一个叫作 Existential Container 的数据结构。
这是一个最普通的 Existential Container。
用伪代码表示以下:
// Swift 伪代码
struct ExistContDrawable {
var valueBuffer: (Int, Int, Int)
var vwt: ValueWitnessTable
var pwt: DrawableProtocolWitnessTable
}
复制代码
因此,对于上文代码中的 Point 和 Line 最后的数据结构大体以下:
这里须要注意的几个点:
对于这个大小差别最主要在于这个 PWT 指针,对于 Any 来讲,没有具体的函数实现,因此不须要 PWT 这个指针,可是对于 ProtocolOne&ProtocolTwo 的组合协议,是须要两个 PWT 指针来表示的。
OK,因为 Existential Container 的引入,咱们能够将协议做为类型来解决 平凡类型 没有继承的问题,因此 Struct:Protocol 和 抽象类就愈来愈像了。
回到咱们最初的疑问,「在 Swift 中的, Struct:Protocol 比 抽象类 好在哪里?」
可是,虽然表面上协议类型确实比抽象类更加的**“好”**,可是我仍是想说,不要随随便便把协议当作类型来使用。
为何这么说?先来看一段代码:
struct Pair {
init(_ f: Drawable, _ s: Drawable) {
first = f ; second = s
}
var first: Drawable
var second: Drawable
}
复制代码
首先,咱们把 Drawable 协议当作一个类型,做为 Pair 的属性,因为协议类型的 value buffer 只有三个 word,因此若是一个 struct(好比上文的Line) 超过三个 word,那么会将值保存到堆中,所以会形成下图的现象:
一个简单的复制,致使属性的copy,从而引发 大量的堆内存分配。
因此,不要随随便便把协议当作类型来使用。上面的状况发生于无形之中,你却没有发现。
固然,若是你非要将协议当作类型也是能够解决的,首先须要把Line改成class而不是struct,目的就是引入引用计数。因此,将Line改成class以后,就变成了以下图所示:
至于修改了 line 的 x1 致使全部 pair 下的 line 的 x1 的值都变了,咱们能够引入 Copy On Write 来解决。
当咱们 Line 使用平凡类型时,因为line占用了4个word,当把协议做为类型时,没法将line存在 value buffer 中,致使了堆内存分配,同时每一次复制都会引起堆内存分配,因此咱们采用了引用类型来替代平凡类型,增长了引用计数而下降了堆内存分配,这就是一个很好的引用计数权衡的问题。
首先,若是咱们把协议当作类型来处理,咱们称之为 「动态多态」,代码以下:
protocol Drawable {
func draw()
}
func drawACopy(local : Drawable) {
local.draw()
}
let line = Line()
drawACopy(line)
// ...
let point = Point()
drawACopy(point)
复制代码
而若是咱们使用泛型来改写的话,咱们称之为 「静态多态」,代码以下:
// Drawing a copy using a generic method
protocol Drawable {
func draw()
}
func drawACopy<T: Drawable>(local : T) {
local.draw()
}
let line = Line()
drawACopy(line)
// ...
let point = Point()
drawACopy(point)
复制代码
而这里所谓的 动态 和 静态 的区别在哪里呢?
在 Xcode 8 以前,惟一的区别就是因为使用了泛型,因此在调度方法是,咱们已经能够根据上下文肯定了这个 T 究竟是什么类型,因此并不须要 Existential Container,因此泛型没有使用 Existential Container,可是由于仍是多态,因此仍是须要VWT和PWT做为隐形参数传递,对于临时变量仍然按照ValueBuffer的逻辑存储 - 分配3个word,若是存储数据大小超过3个word,则在堆上开辟内存存储。如图所示:
这样的形式其实和把协议做为类型并无什么区别。惟一的就是没有 Existential Container 的中间层了。
可是,在 Xcode 8 以后,引入了 Whole-Module Optimization 使泛型的写法更加静态化。
首先,因为能够根据上下文知道肯定的类型,因此编译器会为每个类型都生成一个drawACopy的方法,示例以下:
func drawACopy<T : Drawable>(local : T) {
local.draw()
}
// 编译后
func drawACopyOfALine(local : Line) {
local.draw()
}
func drawACopyOfAPoint(local : Point) {
local.draw()
}
//好比:
drawACopy(local: Point(x: 1.0, y: 1.0))
//变为
drawACopyOfAPoint(local : Point(x: 1.0, y: 1.0))
复制代码
因为每一个类型都生成了一个drawACopy的方法,drawACopyOfAPoint的调用就吧编程了一个静态调度,再根据前文静态调度的时候,编译器会作 inline 处理,因此上面的代码通过编译器处理以后代码以下:
drawACopy(local: Point(x: 1.0, y: 1.0))
//会变为
Point(x: 1.0, y: 1.0).draw()
复制代码
因为编译器一步步的处理,不再须要 vwt、pwt及value buffer了。因此对于泛型来作多态来讲,就叫作静态多态。
工做之余,写了点笔记,若是须要能够在个人 GitHub 看。