12.Swift性能分析

编译器优化

  • 一般的编译过程中会把源文件先生成目标文件(.o文件)然后连接器将不同的目标文件组合起来,生成可执行文件。

  • Swift编译器引入了Whole Module Optimizations优化机制

    • 如果有一个传入泛型的函数funcA,在整个项目中只传入了Int型,那么编译器就不会在编译成支持其他类型的方法了。

    • 类似的优化还有很多,为编译器提供了更多信息,使编译器可以从全局角度出发做更多的优化

内存分配和引用计数优化

一般程序的内存区域,除了代码段和数据段之外,剩下的主要是堆内存和栈内存。

  • 堆(heap)

    • 堆内存一般由程序员自己申请、指明大小、释放,是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或 缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张); 当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

  • 栈 (stack heap)

    • 又称堆栈, 由编译器自动创建/分配/释放,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}” 中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外, 在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值 也会被存放回栈中。由于栈的后进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

基于堆栈的优化

Swift中值类型存放在栈中,应用类型存放在堆中。 苹果也明确指出建议使用值类型。苹果为何这么建议呢?

  • 数据结构

    • 存放在栈里的数据结构较为简单,只有一些值相关的东西

    • 堆上的数据结构较为复杂,包含了type、retainCount等

  • 数据的分配与读取

    • 存放在栈中的数据从栈区底部推入 (push),从栈区顶部弹出 (pop),类似一个数据结构中的栈。由于我们只能够修改栈的末端,因此我们可以通过维护一个指向栈末端的指针来实现这种数据结构,并且在其中进行内存的分配和释放只需要重新分配该整数即可。所以栈上分配和释放内存的代价很小。

    • 存放在堆中的数据并不是直接 push/pop,类似数据结构中的链表,需要通过一定的算法找出最优的未使用的内存块,再存放数据。同时销毁内存时也需要重新插值。

  • 多线程处理

    • 栈,是线程独有的不用考虑多线程问题

    • 堆,数据时多线程共享的,需要同步锁来解决这个问题

所以基于内存分配方面的考虑,应该更多的使用栈而不是堆,达到优化效果

值类型的一个优化

有这样一个结构体

Struct User {
    var avatar = UIImage。。。
    var name = "老王"
}

var userA = User()
var userB = userA

这样结构体中的引用类型的引用计数会增加 如果我们对其中的引用类型再进行一层封装的话就可以减少不必要的引用计数

方法调用的优化

  • Objective-C 中方法的调用,从本质上来说都是向相应的对象发送消息。方法经编译器编译过后一般就变成了objc_msgSend函数,该函数的第一个参数是接受消息的对象,第二个参数是消息的名字,后面的都是消息携带的名字,参数从0到 n 个不等。

  • 正是基于这一点Objective-C 中,我们可以字符串去调用方法,就可以用变量来传递这个字符串,进而可以实现一些运行时动态调用,语言提供的 NSSelectorFromString 是一个很好的说明,runtime 也因此被开发者奉为神器,被广大开发这熟知的JSPatch 也是基于这点实现的。因为这种动态性的设计使得Objective-C 语言变得异常灵活。

  • 但是,Objective-C语言动态化这种灵活性是以查表的方式找出函数地址,既然查表操作,当然要付出时间代价。苹果官网文档中介绍了方法调用时,函数地址查询过程,苹果也发现了这种方式调用起来会很慢,所以一种这种的办法就是缓存方法调用的查询结果,但即便是这样,性能上同将函数地址硬编码到代码中这种方式相比还是有一些差距。

  • 相比于Objective-C,Swift语言直接放弃了Objective-C这个动态化机制。就这一方面而言,Swift如今算是和很多主流语言保持了一直。因为舍弃了动态特性,Swift语言势必比Objective-C快了一些,但在一定程度上丢失了灵活性。相信不久的将来,Swift势必会引入一些动态特性,不过目前而言这并不是它的首要目标。

协议类型

如有下面两个协议

protocol MyProtocol

struct Owner: MyProtocol {
    var name: String
}

struct User: MyProtocol {
    var name: String
    var url: String
}

let userA = User()
let owner = Owner()


var array: [MyProtocol] = [userA, owner]

这里的数组内的元素是不同类型的,占用空间不一致。无法高效的定位元素位置。

  • 苹果的解决方案

    • Existential Container

      • 这是一个额外的容器,大小一定

        • 分为5段(word)

          • 前三段用来存储元数据的值,如果存储不下则会在堆内存上开辟空间存放,而在第一段中存入指针

          • 第四段:Value Witness Table(VWT)。每个类型都对应这样一个表,用来存储值的创建,释放,拷贝等操作函数。(管理 Existential Container 生命周期)

          • 第五段:Protocol Witness Table(PWT),用于存放协议(Protocol)对应的函数的实现函数地址

总结

  • 为什么Swift编译很慢?

    • 因为Swift在编译的时候做了很多事情,所以消耗时间比较多是正常的。如对类型的分析等。

  • 为什么Swift相比较OC会更快?

    • 编译器 Whole Module Optimizations 机制的全局优化、更多的栈内存分配、更少的引用计数、更多的静态、协议类型的使用等都是Swift比OC更快的原因。

参考

https://www.jianshu.com/p/0ca59322c08b

Last updated