由于Swift是一门静态的、强类型的语言,因此Swift编译器能够在编译阶段就对代码做大量优化,本文将探讨Swift编译器会对代码做哪些优化,我们该如何利用编译器的优化特性写出更高效的Swift代码。
Premature optimization is the root of all evil.
—–by Donald Knut
显式和隐式性能优化
只有当你发现了性能问题,并找到了原因之后,才应该开始做性能优化。
性能优化有两种类型:
- 显式的
- 隐式的
显式的性能优化直接指向低效的代码,这类优化通常要求对代码做重大的修改,比如换一种更加高效的算法。这通常会降低代码的可读性。使用更多的内存做缓存也能提高性能。
隐式的性能优化则需要借助语言特性来提高性能。它通常不需要对代码做重大修改,对代码可读性也没有负面影响,有时甚至能提高可读性。它被称为隐式是因为你能在代码的任何地方使用它,但过一段时间之后你又不会感觉到它的存在。
这里主要将探讨如何利用Swift的语言特性来做隐式的性能优化。
使用常量代替变量
相比于变量,使用常量拥有更高的性能。因为常量能清晰地告诉编译器,这个变量的值将不会被改变,编译器就会对它做内联优化,并不会为它分配内存空间,这样既能节省内存空间,也能提高运行速度。
方法调用
两种主要的方法调用类型:
- 静态: 静态方法绑定是指当你调用一个方法时,编译器知道你确实是在调用这个类的这个方法。
- 动态: 动态方法绑定在方法和对象之间是一种弱绑定。当你调用一个对象的某个方法时,无法保证这个对象一定就能执行这个方法。
Objective-C就是一种动态类型语言,并且拥有动态运行时。它采用消息发送机制来调用方法。当你调用一个方法时,实际上是发送了一个消息给这个对象,这个对象接受到这个消息后,必须先检查一下它是否实现了这个方法。
而Swift使用的是静态方法绑定,它拥有一个虚方法表,记录了一个类型拥有的所有函数指针。当在Swift中调用一个方法时,能够直接拿到这个方法的内存地址,然后执行它。这样就不需要去检查是否实现了这个方法。
全局函数(Global function)和静态方法(Static method)的执行速度是最快的。
类方法(Class method)分为两种情况,一种是不能被重写时,它用于和全局函数和静态方法一样的执行效率,一种是能够被重写时,当方法调用者在编译时是不确定的,那编译器就不能对它做优化,它的执行效率就会稍差。比如下面这个例子:
1 | class SuperClass { |
如果给类方法或者给类加上final
修饰词,表明这个类或方法不能被继承重写,那这个类方法也将拥有和全局函数、静态方法一样的性能。
实例方法和类方法相似,因为有继承重写的因素,编译器能否对实例方法进行优化,取决于编译器能否确切地知道实例方法的具体调用者。
Intelligent code
因为Swift是一门静态的、强类型的语言,因此它会尝试去移除所有不必要的代码执行,比如那些没有被使用过的代码,或者对应用没有任何实际影响的代码。
1 |
|
在这段代码中,doNothing
方法没有做任何事,Swift编译器就会自动移除Test().doNothing()
这两句代码。
到OC就没有这种优化,因为OC可以在运行时可以改变一个方法的实际调用和实现。
但是Swift编译器不能自动忽略print
方法,所以我们在程序里通过print
打log时,可以采用只有在debug模式下执行print
方法的方式,这样在release模式下,编译器就能忽略print
方法了。
Using nonoptimizable constants
在swift中,有些类型在类中使用时,不能和其它类型一样拥有上面的优化特性,即使是使用了常量。这些类型有:
- String
- Array
- Custom Class objects
- Closures
- Set
- Dictionary
以上几种类型因为要暴露给OC使用,内部会维持一个OC的相应类型,比如Sting
对应NSString
。这样就拥有了一些OC的运行时特性,因此编译器无法自动忽略它们。
但如果是在struct
中使用这些类型,那这些类型就不会暴露给OC使用,编译器就又能为它们做优化了。
一些提高代码速度的方法
- 使用
final
- 使用
@inline(__always)
是函数始终编译成内联函数 - 使用不可变的值类型
- 避免在Swift代码中使用OC类型
- 避免暴露Swift代码给OC使用
- 避免使用OC的动态特性