理解Swift中struct和class在不同情况下性能的差异

如果你被问起Swift中struct和class有什么不同的时候你会怎么回答?
我想大多数人的第一反应应该是struct是值类型class是引用类型他俩在语义上面不同。在想其他可能就是他们之间的相同点了,比方都可以用来定义类型,都可以有property,都可以有method,所以单从语法上理解他们没什么意义,其实单从语义不同这一条就可以延伸出很多,从初始化方式,内存管理,方法派发方式都是截然不同,当然这不是我们本次讨论的重点,本次主要讨论struct和class在不同情况下性能的差异

我们主要从三个性能维度来比较struct和class的性能差异

  • 内存分配
  • 引用计数
  • 方法派发

内存分配

内存分配分为栈内存分配和堆内存分配两种

栈内存的存储结构比较简单,你可以简单的理解为push到栈底pop出来这么简单,而要做的就是通过移动栈针来分配和销毁内存

堆内存相比栈有着更为复杂的存储结构,他的分配方式你可以理解为在堆中寻找合适大小的空闲内存块来分配内存,把内存块重新插入堆来销毁内存,当然这些仅仅只是堆内存相比栈内存性能消耗大的一个方面,更重要的是堆内存支持多线程操作,相应的就要通过同步等方式保证线程的安全性。

下面我们从代码角度来说一下struc和class在内存分配方面的性能差异

struct Point {
   var x, y: Double
   func draw() { ... }
}
let point1 = Point(x: 0, y: 0)
var point2 = point1
point2.x = 5
// use `point1`
// use `point2`

定义一个struct Point 含有property x,y method draw

首先在代码执行之前编译器会在栈上分配一块4个word大小的内存,分配的过程就是我在上面提到的移动栈针



用(0,0)来初始化point1 并把point1赋值给point2,你可以理解为将value拷贝到分配好的栈内存上,因为struct是值类型所以point1和point2是两个独立的instance



修改point2.x不会影响point1

最后use point1 use point2离开作用域 移动栈针,销毁栈内存

接下来我们看一下class的情况

class Point {
   var x, y: Double
   func draw() { ... }
}
let point1 = Point(x: 0, y: 0)
let point2 = point1
point2.x = 5
// use `point1`
// use `point2`

只是把struct换成class其他地方不变
首先在代码执行之前编译器会在栈上分配两个word大小的内存,不过这一次不是用来存储Point的property


代码执行初始化point1在堆上寻找合适大小的内存块分配

然后将value拷贝到堆内存存储,并在栈上存储一个指向这块堆内存的指针

可以看到堆上分配的是4个word大小的内存块,剩余的两个蓝色格子存放的是关于class生命周期相关函数表的指针
let point2 = point1执行的是引用语言,只是在point2的栈内存存储了一个指向point1堆内存的指针

修改point2.x也会影响到point1

最后use point1 use point2 离开作用域,堆内存销毁(查找并把内存块重新插入到栈内存中),栈内存销毁(移动栈针)
当然栈内存在分配的过程中还要保证线程安全(这也是一笔很大的开销)

怎么样,是不是觉得上述情况下Point用struc表示比class表示在内存分配方面要快很多

下面看一个在实际应用中的优化方案

enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }

var cache = [String : UIImage]()

func makeBalloon(_ color: Color, orientation: Orientation, tail: Tail) -> UIImage {
let key = "\(color):\(orientation):\(tail)"
   if let image = cache[key] {
      return image
   }
   ...
}

这段代码的应用场景是我们常见的iMessage聊天界面气泡生产能function,三个enum表示气泡的不同情况,比方蓝色朝左带尾巴,因为用户可能经常滑动聊天列表来查看消息makeBalloon这个方法的调用时非常频繁的,为了避免多次生成UIImage,我们用Dictionary来充当一个cache,用不同情况下的enum序列化一个String来充当cache的key。

思考一下这样写有什么问题么?
问题就出在key上面,用String充当key,首先在类型安全上面无法保证存放的一定是一个气泡的类型,因为是String所以你可以存放阿猫阿狗等等。其次String的character是存储在堆上面的,所以每次调用makeBalloon虽然命中cache,但仍然不停的设计堆内存分配和销毁

怎么解决?

struct Attributes : Hashable {
   var color: Color
   var orientation: Orientation
   var tail: Tail
}

 let key = Attributes(color: color, orientation: orientation,tail: tail)

看到优化代码你应该恍然大悟,原来Swift已经为我们准备好了更好地类型struct并且他可以作为Dictionary的key,这样就不涉及任何堆内存分配销毁,并且Attributes可以确保类型安全

引用计数

Swift是如何知道堆内存是安全的并且可以释放了呢?答案就是引用计数,原理就是在instance内部插入一个refCount的property来管理引用计数,新增引用refCount+1,引用销毁refCount-1,refCount为0时,Swift就知道了存储instance的这块内存是安全的可以释放。

引用计数方面的性能消耗当然不是加一减一那么简单,首先是一对间接调用retain和release,其次引用计数支持多线程,所以同样需要保证线程安全

我们先来看下class的情况,下面是一段伪代码,旨在解释清楚引用计数的原理

class Point {
   var refCount: Int
   var x, y: Double
   func draw() { ... }
}
let point1 = Point(x: 0, y: 0)
let point2 = point1
retain(point2)
point2.x = 5
// use `point1`
release(point1)
// use `point2`
release(point2)

内存分配方面不再赘述,let point1 = Point(x: 0, y: 0)这块内存的refCount 为1
retain(point2)refCount变为2 然后use point1 离开作用域release(point1) refCount 减为1release(point2)refCount减为0 然后销毁内存

从上一个内存分配的环节可知struct Point的内存分配均为栈内存分配,它不涉及任何引用计数。

那如果struct中存有引用类型的property呢?

struct Label {
   var text: String
   var font: UIFont
   func draw() { ... }
}
let label1 = Label(text: "Hi", font: font)
let label2 = label1
// use `label1`
// use `label2`

上面的代码中定义了一个struct Lable 含有一个String值类型但是character存储在堆上,UIFont是一个class 存储在堆上
let label1 = Label(text: "Hi", font: font)初始化label1 看一下会发生什么


let label2 = label1lable1赋值给label2

看到了吧,一共有四次引用计数操作,也就是会有六个调用,我们看一下这个过程的伪代码

struct Label {
   var text: String
   var font: UIFont
   func draw() { ... }
}
let label1 = Label(text: "Hi", font: font)
let label2 = label1
retain(label2.text._storage)
retain(label2.font)
// use label1
release(label1.text._storage)
release(label1.font)
// use label2
release(label2.text._storage)
release(label2.font)

在引用计数这个维度你想到了什么?
结论就是
在引用计数这个维度不包含引用属性的struct性能消耗最小,因为他不涉及引用计数,含有一个引用类型的struct性能消耗等于class,含有两个及以上引用类型property的struct在引用计数方面的性能消耗是class的整数倍

下面看一个在实际应用中的优化方案

struct Attachment {
   let fileURL: URL
   let uuid: String
   let mineType: String
 
  init?(fileURL: URL, uuid: String, mimeType: String) {
      guard mineType.isMineType
      else { return nil }
    
      self.fileURL = fileURL
      self.uuid = uuid
      self.mineType = mimeType
} }

这段代码是我们给message新建的一个model,表示我们聊天时发送的文件,它含有一个URL类型的fileURL表示文件的位置,一个String类型的uuid方便我们在不同设备定位这个文件,因为不可能支持所有的文件格式,所以有一个String类型的mineType用于过滤不支持的文件格式

想一下这么创建model会有什么性能上的问题或者有什么优化的方案么?
来看一下Attachment的存储情况


因为URL为引用类型,String的character存储在堆上,所以Attachment在引用计数的层面消耗有点大,感觉可以优化,那怎么优化?

首先uuid声明为String类型真的好么?答案是不好,一是无法在类型上限制uuid的安全性,二是涉及到引用计数,那怎么优化呢?在16年Swift Fundation中加入了UUID这个类型,是一个值类型不涉及引用计数,也能在类型安全上让我们满意,再来看一下mineType

extension String {
   var isMimeType: Bool {
      switch self {
      case "image/jpeg":
         return true
      case "image/png":
         return true
      case "image/gif":
         return true
      default:
         return false
      }
   }
}

我们是通过给String扩展的方式来过滤我们支持的文件格式,看到这里你肯定想到了Swift为我们提供的更好的表示多种情况的抽象类型enum吧,它同为值类型,不涉及引用计数(enum在个别情况下可以作为引用类型,这里暂时不讨论)

enum MimeType : String {
   case jpeg = "image/jpeg"
   case png = "image/png"
   case gif = "image/gif"
}

优化后的模型:

struct Attachment {
   let fileURL: URL
   let uuid: UUID
   let mimeType: MimeType
   init?(fileURL: URL, uuid: UUID, mimeType: String) {
      guard let mimeType = MimeType(rawValue: mimeType)
      else { return nil }
      self.fileURL = fileURL
      self.uuid = uuid
      self.mimeType = mimeType
} }

优化后的model在达到了最佳状态,看完这个例子后不妨瞅瞅自己写过的代码,是否也有这种可以优化的地方

方法派发

静态派发
编译器将函数地址直接编码在汇编中,调用的时候根据地址直接跳转到实现,编译器可以进行内联等优化(struct都是静态派发)

动态派发
运行时查找函数表,找到后再跳转到实现,当然,动态派发仅仅多一个一个查表环节并不是他慢的原因,真正的原因是他阻止了编译器可以进行的内联等优化手段(class派发情况比较特殊,这里我们只讨论动态派发的情况)

看一个内联优化的例子

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. drawAPoint(point)
  2. point.draw()
  3. // Point.draw implementation
    因为struct中的method是静态派发,也就是说编译器就知道函数的实现地址,那么它可以进行第一步优化
  4. point.draw()
  5. // Point.draw implementation
    当然还可以进行第二步优化
  6. // Point.draw implementation
    看到了吧。通过编译器的内联优化可以省略掉两步调用,避免一部分压栈弹栈等函数调用的开销。直接跳转到函数的实现,理解了静态派发快的原因了吧

如果是把上述情况换为class,那编译器就无法进行内联优化,从方法派发这个维度来说struct的静态派发更胜一筹

资料参考2016WWDC416

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,488评论 3 83
  • 第 23 条:通过委托与数据源协议进行对象间通信 Objective-C 可以使用 “委托模式”(Delegate...
    德山_阅读 830评论 0 2
  • 一、两个疑惑 OC 和 Swift 语言在 Richards 上评测的结果显示,Swift 比 OC 快了4倍,S...
    ZhengYaWei阅读 12,363评论 4 29
  • 语,即言语,说话,工具也,追求实用价值;文,即文学,文化,人文也,追求艺术价值。工具人文兼顾,实用艺术兼修,不可求...
    旺旺_0dcb阅读 306评论 1 2
  • 1. 生活在社交人群当中必然要求人们相互迁就和忍让;因此,人们聚会的场面越大,就越容易变得枯燥乏味。 2. 欲望是...
    木卯丁阅读 297评论 0 2