Swift 最佳实践之 High Performance

Swift 作为现代、高效、安全的编程语言,其背后有很多高级特性为之支撑。

『 Swift 最佳实践 』系列对常用的语言特性逐个进行介绍,助力写出更简洁、更优雅的 Swift 代码,快速实现从 OC 到 Swift 的转变。

该系列内容主要包括:

ps. 本系列不是入门级语法教程,需要有一定的 Swift 基础

本文是系列文章的第十篇,主要介绍 Swift Method Dispatch 以及一些常见的 Swift 性能优化方法。

Overview


Swift is a general-purpose programming language built using a modern approach to safety, performance, and software design patterns.

Swift.org – About Swift

Swift 作为一门现代语言,具有安全、高效等特点,然而在其延生之初就背上了兼容 OC 的沉重包袱,使得其在设计上不得不做出一些让步和妥协。??

其中,Method Dispatch 就是例证之一,今天的分享就从 Swift Method Dispatch 开始!

Swift Method Dispatch


一次方法调用总共分 ③ 步 ( inline 除外 ):

  • 找到方法入口地址
  • 跳转
  • 返回

其中,最重要的一步就是找到方法的入口地址,总的来说有 2 种方式:

  • 静态调用,编译-链接期间确定跳转地址

    • 编译时生成符号表
    • 链接时查表找到跳转地址

在最终生成的可执行文件中跳转地址是确定的

对上述过程感兴趣的同学可以看看 程序员的自我修养 — 链接、装载与库

  • 动态调用,运行时查表,这也是 OOP 中 「 多态 (Polymorphism) 」 得以实现的基础,不同语言在实现上又有所区别:

    • 像 C++ 中有虚函数表 (VMT),所有虚函数都会记录到 VMT,调用时查表找到入口地址

      对 C++ Object Model、VMT 感兴趣的同学可以看看 深度探索 C++ 对象模型

    • Objective-C 中有 method_list,所有的方法都会记录到 method_list 中

下图简要总结了静态调用VMT 调用Message Dispatch 的过程与区别:

method-call.png

很明显,静态调用的性能比动态调用好,但灵活性不足。有得必有失 ?

Swift 支持上述 3 种调用方式!? ?

那么 Swift 中上述 3 种调用方式分别发生在什么情况下❓

一句话,不需要 「动态性」 的情况下就会用静态调用 ???:

方法调用的「动态性」本质上是指,具体执行方法的哪个实现需要到运行时才能确定,而非编译期间就指定的,如:多态、swizzle。

  • Value type:structenum,因为它们不支持继承,也就没有动态性可言。

    故,值类型的方法调用都是静态调用

  • Protocol extension: 通过 Existential Type 调用 Protocol extension 中实现的方法

    如:

    protocol SomeProtocol {}
    
    
    
    extension SomeProtocol {
      func bar() {
        print("Hello world!")
    
      }
    
    }
    
    
    
    
    
    
    func doSomething(_ sp: any SomeProtocol) {
      sp.bar()   // ? 静态调用
    }
    
  • Class extension: 由于 Swift extension 不允许重写主类中的方法,故 Class extension 中的方法都是静态调用

    来自 OC 基类的方法以及用 @obj dynamic 修饰的方法除外

    如:

    class SomeClass {}
    
    
    
    extension SomeClass {
      func bar() {    // ? 静态调用
        print("Hello world!")
    
      }
    
    }
    
    
    
    
    
  • final:final 修饰的类不能被继承、修饰的方法不能被重写 (override),故它们都是静态调用

  • private/privatefile 私有性使得编译器在编译时可以判断是否有继承、重写的操作,若没有,进而用静态调用替换间接调用,如:

    private class  SomePrivateClass {
      func doSomething() { ... }
    }
    
    class SomeClass {
      fileprivate func filePrivateMethod() { ... }
    }
    
    
    
    
    
    
    func doSomething(_ obj: SomePrivateClass) {
      // 由于在当前文件中没有 SomePrivateClass 的子类
      // 也就是 SomePrivateClass 不可能有子类,因其 private 的可见性
      // 故编译器可以「放心地」将方法 doSomething 的调用改为静态调用
      //
      obj.doSomething()
    }
    
    func doSomething(_ obj: SomeClass) {
      // 同理
      //
      obj.filePrivateMethod()
    }
    
  • internal 在开启 Whole-Module Optimization (WMO) 时,可见性为 internal 的方法也可能被编译器优化为静态调用,原理与上述 private/privatefile 一样

除以上情况外,都是动态调用 ?

如前所述,Swift 中动态调用又分为查表 (vtable/witness_table) 和 OC Message Dispatch:

  • Swift 中继承并重写 OC 基类的方法,通过 Message Dispatch 调用

  • dynamic + @objc 关键字修饰的方法通过 Message Dispatch 调用

    • dynamic 关键字的含义是用动态派发 (Dynamic Dispatch)
    • @objc 表示对 OC 可见
    • 2 者没有任何关联、交集
    • 然而,Swift 并没有什么 Dynamic Dispatch 一说,必须借助 OC Message Disaptch。因此,dynamic 需要配合 @objc 一起使用才有效。
    • dynamic + @objc 主要用于 KVO、swizzling 等
  • 其他情况都是查表

下面通过一个例子、一个表格来总结一下 Swift Method Disaptch 的所有情况:

methoddispatch.png

methoddispatchtable.png

High Performance


减少动态调用

上一小节详细介绍了 Swift Method Dispatch,我们知道静态调用比动态调用性能更好。

因此,尽量用静态调用代替动态调用,我们可以做的有:

  • 充分利用 privatefileprivate 以及 internal

我个人觉得,privatefileprivate 以及 internal 对代码可维护性、对接口语义的完善比性能更重要!

因此,不要放过任何一次能用它们的机会!

充分利用泛型特化 (Generic Specialization)

Swift 最佳实践之 Generics 一文中,我们讲过编译器会对 Generic 做特化优化,其前提是:

  • 泛型方法与调用方在同一个源文件,一起进行编译
  • 开启 Whole-Module Optimization 时,同一模块内部的泛型调用也可以被特化

因此,在设计允许下尽量将泛型方法定义与调用放在同一文件或模块内

对于只有 class 能实现的协议加上 AnyObject 限制

在开发中,可能有些 protocol 只希望 class 去实现

此时,可以在 protocol 定义时加上 AnyObject 限制,如:

protocol SomeProtocol: AnyObject {}

对于有 AnyObject 修饰的 protocol,编译器在实现时可以做很多优化,因为可以排除其背后的实现是 Value-Type 的可能。

比如,在内存管理上可以放心的用 ARC,而不用考虑 Value-Type copy 等问题。

Swift 最佳实践之 Protocol 中介绍过,non-class constraint protocol 与 class constraint protocol (: AnyObject) 的 Existential Container 不一样:

// for non-class constraint protocol
//

struct OpaqueExistentialContainer {
  void *fixedSizeBuffer[3];
  Metadata *type;
  WitnessTable *witnessTables[NUM_WITNESS_TABLES];
}




// for class constraint protocol
//

struct ClassExistentialContainer {
  HeapObject *value;
  WitnessTable *witnessTables[NUM_WITNESS_TABLES];
}

Collections

函数式编程 (Functional programming) 思想在 Swift 中随处可见,在 Collection 上也有大量函数式操作符。

优先考虑使用函数式操作符处理集合,如:

let values = [1, 2, 3]


values.forEach { ... }         // ✅
for value in values { ... }    // ❌

contains > first(where:)

在判断集合中是否存在某个元素时优先使用 contains,如:

let values = [1, 2, 3]


let result = values.contains(1)                     // ✅
let result1 = values.first { $0 == 1 } != nil       // ❌
let result2 = !values.filter { $0 == 1 }.isEmpty    // ❌

isEmpty > count

When you need to check whether your collection is empty, use the isEmpty property instead of checking that the count property is equal to zero. For collections that don’t conform to RandomAccessCollection, accessing the count property iterates through the elements of the collection.

isEmpty – Apple Developer Documentation

判断集合是否为空时,优先使用 isEmpty,其时间复杂度为 O(1),而对于 Array 等非 RandomAccessCollection 的集合 count 需要遍历,时间复杂度为 O(n)

如:

let values = [1, 2, 3]


let result = values.isEmpty           // ✅
let result1 = values.count != 0       // ❌

first(where:) > filter

查找第一个满足条件的元素时优先使用 first(where:),如:

let result = values.first { $0 > 1 }            // ✅
let result2 = values.filter { $0 > 1 }.first    // ❌

allSatisfy > reduce

判断集合中所有元素是否都满足某个条件时,优先使用allSatisfy,如:

let result = values.allSatisfy { $0 > 0 }                  // ✅



let result1 = reduce(true) { partialResult, value in       // ❌
  return partialResult && value > 0
}
let result2 = values.filter { $0 <= 0 }.isEmpty            // ❌

slice > sub collection

正如,Swift 最佳实践之 Advanced Collections 中所讲到,对集合做 slicing 不会有内存分配、copy 元素的操作,切片与原始集合共用同一块内存,切片的时间复杂度为 O(1)

ArraySlice.png

因此,优先考虑使用 Slice,而非创建子集合,如:

let values = [1, 2, 3, 4, 5, 6]
let slice = values[2...4]    // ✅

var sub = [Int]()            // ❌
for i in 2...4 {
  sub.append(values[i])
}




数组元素优先考虑 Value-type

Swift Array 其实现需要考虑背后是 NSArray 的可能性

为了处理 NSArrayArray 有一些额外的桥接工作要做

然而,NSArray 的元素类型一定是 Reference-type

因此,如果 Array 的元素类型是 Value-type,就可以排除是 NSArray 的可能性,此时编译器就可以优化掉相关的桥接工作,如:

struct Student {  // ? struct ✅ class ❌
  let name: String
  let age: Int
}

var students: [Student]  // ?

valuetypearrayvsreferencetypearray.png

需要注意的是,值类型涉及 copy、move 等问题,在数据量很大时不一定比引用类型有优势,因为通常情况下引用类型 copy 的是指针,需要 case-by-case 处理。

小结

为了兼顾性能、灵活性以及兼容 OC,Swift 存在 3 种方法派发方式:Static、Table、Message。它们有着不同的性能表现,在开发中我们要尽量使用静态派发。

通过设置可继承性 (final)、可见性 (privatefileprivateinternal) 等,可以提高静态派发的可能性。

我个人觉得,它们的意义不仅仅是提升性能,更重要的是可以提高代码的可维护性、完善接口语义!

另外,函数式编程思想在 Swift 中有充分的体现,在日常开发中我们要转变思维,善于利用 Swift 提供的函数式基础设施。

至此,共 10 期的『 Swift 最佳实践 』系列分享告一段落,希望对正在学习、使用 Swift 的同学有所帮助❗️?

参考资料

swift/docs/OptimizationTips.rst at main · apple/swift · GitHub

What’s .self, .Type and .Protocol? Understanding Swift Metatypes

www.mikeash.com/pyblog/frid…

varun04tomar.medium.com/to-the-dept…

@objc and dynamic – Swift Unboxed

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYYtBLEJ' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片