优化Swift性能 (Optimizing Swift Performance)



主要从三个方面进行优化:

  • 引用计数(Reference Counting)
  • 泛型(Generics)
  • 动态分发(Dynamic Dispatch)

最后,也会介绍一下如何使用Time Profiler检测耗时操作。



引用计数(Reference Counting)



使用类定义一个数据类型,在对这个数据类型进行大量重复操作时,引用计数对于性能的影响将会变得很明显:

class Point {
    var x, y: Float
}

var array: [Point] = ...
for p in array {
    increment
    ...
    decrement
}

使用struct来解决引用计数导致的性能问题:

struct Point {
    var x, y: Float
}



当struct中也有引用类型时,比如:

1306450-4c9460a1c96a9728.png
包含多个引用的struct

可以用一个类来包裹这个struct,这样就可以避免对struct中的引用对象进行单独的引用计数:


1306450-14d7cfa5b8408011.png
用一个类来包裹含有引用对象的struct




泛型(Generics)


观察以下泛型代码:

func min<T: Comparable>(x: T, y: T) -> T {
    return y < x ? y : x
}



在编译时,以上泛型代码其实是被翻译成了类似这种结构的代码:

1306450-6173fdc5a4f8fcbe.png
泛型需要进行转换后,才能进行后续操作



当泛型函数在被调用时,编译器会将泛型具体化(Generic Specialization):

1306450-4a77d90e452c6eff.png
泛型具体化

最后被具体化为如下形式:


1306450-75446ddae8aaa013.png
泛型被具体化为Int类型

但是泛型定义的可见性会影响泛型的具体化,比如在同一个module下的不同文件中:


1306450-b9fc3a7375d3f486.png
调用泛型函数的文件无法知晓泛型函数的定义

如何解决泛型定义的可见性问题?

打开项目设置,Build Settings - Optimization Level,将Release模式的选项改为 Whole Module Optimization。此时,同个Module下不同文件中的泛型函数,在开启Whole Module Optimization之后可以一起编译,此时泛型定义对于同个Module中的不同文件是可见的。




动态分发(Dynamic Dispatch)



这里涉及到两个概念:

  • 继承
  • 访问控制


继承

请观察以下类型的定义:


1306450-3948a8d218533097.png

在获取name属性的值和调用noise()方法时,都需要进行额外的操作,因为编译器不确定属性和方法有没有被子类重写。



如何消除额外的操作?

1306450-c76f1dac5f811ac7.png
使用final关键字定义var name

使用final关键字,明确地告诉编译器不存在重写操作。




访问控制

再来观察另一个例子:


1306450-59948402648a1211.png
func noiseImpl() 方法

—— 如果调用noiseImpl()方法,编译器能不能直接调用Pet中的noiseImpl()方法?
—— 当然不能!

在这里,编译器会假设子类中有可能重写这个方法,所以它不会直接调用Pet中的noiseImpl()方法。
这时候,可以通过将noiseImpl()方法定义为private级别来告诉编译器,直接调用Pet中的noiseImpl()方法。

1306450-552145c044d633dc.png
将方法定义为private级别



对于通过module中的类继承,同样可以开启Whole Module Optimization来避免多余的间接调用。编译器可以自行判定当前Module中的继承链关系。

1306450-404829a37ccf059b.png
开启Whole Module Optimization前,Dog中的bark方法需要从继承链中找出合适的noise方法
1306450-9c24e02b62038ed4.png
开启Whole Module Optimization后,Dog中的bark方法直接调用Dog中的noise方法




使用Time Profiler检测耗时操作

新建一个iOS项目,将Point定义为class类型,然后在ViewController中放置一个按钮,点击按钮即可执行iteratePoints操作。

1306450-13f797720c5c13b2.png
在ViewController中放置一个按钮
class Point {
    var x, y: Float
    init(x: Float, y: Float) {
        self.x = x
        self.y = y
    }
}

class ViewController: UIViewController {

    let points = [Point].init(repeating: Point.init(x: 0, y: 0), count: Int(1e7))
    var countX: Float = 0

    @IBAction func iteratePoints(_ sender: Any) {
        for _ in 0..<5 {
            for point in points {
                let p = point
                countX += p.x
            }
        }
    }
}

代码添加完毕,然后在Xcode中按下Command + I即可启动Profile,或者可以点击Xcode菜单栏Product - Profile。弹出Instruments后,双击启动Time Profiler。

1306450-71c797532ac6b6f7.png
启动Profile
1306450-95f562b23e4e641e.png
启动Time Profiler
1306450-a3234839add950a2.png
点击启动按钮



等待CPU占用率变为0,防止其他操作对待检测的操作造成影响。如下图红色区域:

1306450-ee80809fcfeaf7ff.png
等待CPU占用率变为0(蓝色柱消失)

然后,点击ViewController中的iteratePoints按钮,即可看到Time Profiler中有明显的蓝色柱出现。


1306450-67159192cf74d369.png
明显的蓝色柱

点击该区域的蓝色柱,然后按下Command和+按钮,对该区域进行放大。


1306450-f51f03f2118773c7.png
放大后的效果

拖动选中想要检查的部分:


1306450-f2c20a8bb89adad1.png
选中CPU占用率很高的区域

可以发现底部有部分视图有变化,726 @objc ViewController.iteratePoints会变为白色,点击它!


1306450-275cb819be173f4c.png
点击 726 @objc ViewController.iteratePoints



点击展开@objc ViewController.iteratePoints(:),会看到主要的耗时操作为retain/release

1306450-d4ef42681267c30c.png
耗时操作为retain/release



现在,关闭当前的Time Profiler(一定要关闭,不需要保存)

1306450-649fcf4e82666ca6.png
关闭当前的Time Profiler




接下来请修改代码,将Point由class类型改为struct类型。
然后再次按下Command + I,Xcode会用最新的代码更新Build内容,然后启动Profile。
按照之前的操作流程进行操作后,你会发现,耗时操作不再是retain/release。

1306450-b770e402693b5e3f.png
耗时操作为ViewController.iteratePoints(:)



参考文章页面内有视频,视频结尾部分还有很多关于使用Time Profiler进行Swift代码优化的示例(虽然并没有提供示例代码),建议亲自观看。




总结

  • 使用final关键字和访问控制

    1. 帮助编译器理解你的类继承结构;
    2. 已有的代码可能需要因此而更新;
  • 开启Whole Module Optimization

  • 使用Instruments - Time Profiler来检测耗时操作,针对性地进行优化






参考文章:
Optimizing Swift Performance

猜你喜欢

转载自blog.csdn.net/weixin_34384915/article/details/87588343