WWDC2016: 416 Understanding Swift Performance

主要内容

讲解了Swift中对引用类型和值类型的内存管理方式,并提出一些优化建议。

影响性能的重要因素

  1. 栈内存 vs 堆内存
    栈内存的分配和释放仅仅是通过移动栈指针来实现的。而堆内存则要根据申请的大小在堆中寻找合适的位置,释放时需要将内存块回收到堆中,并且要考虑线程安全,所以会慢得多。Swift中的各种基本数据类型和容器(如Int, String, Array, Dictionary等)都是struct,编译器会尽可能用栈的方式来为它们分配内存。
  2. 引用计数
    编译器会自动在我们的代码中加入retainrelease的调用。这两个函数的调用频率极高且涉及线程安全的控制,使其有可能成为性能瓶颈。
  3. 动态函数调用 vs 静态函数调用
    动态函数调用就是在运行时才确定函数名对应的函数地址。对于动态调用,程序必须动态地查询函数地址,而无法在编译期使用inline等优化手段,会相对慢一些。

优化

struct作为Dictionary的key

Paste_Image.png

此处把3个数据点序列化为一个String,并以此为key。这样做有两个缺点:

  1. 使用String为key意味着可以传入任意String,哪怕不是这3个数据点的序列化结果,比如"abc"或"123"。
  2. String本身虽然是struct,但它内部储存字符的数组却是分配在堆上的。每次序列化意味着一次堆操作。
Paste_Image.png

改为struct可以优雅地解决这两个问题。

尽量使用(纯)值类型

Paste_Image.png

值类型如果包含引用类型的数据成员,则程序仍需为此数据成员进行引用计数的维护和堆操作。
所以,尽可能地使用“纯值”类型(这是我造的词,表示不包含引用类型的值类型,编译器可以实现完全的栈内存管理)。比如以下结构中,有两个String成员,程序需为它们进行堆操作。

Paste_Image.png

把它改成UUIDenum类型

Paste_Image.png

注意:此处的enum的源数据类型虽然是String,但是它在内存并不是持有一个String(虽然没说,我想应该只是个Int)。
所以整个结构变成了纯值类型。

感谢 小刚_aea8 的订正。此处的Attachment由于还包含了URL类型的成员,所以它并没有变成一个“纯值”类型。

在不需要多态时,使用泛型代替protocol

先说下多态的基本实现原理。

Paste_Image.png

上面的代码�是通过override的方式来实现多态的。编译器为每个对象添加了一个type成员,里面是这个具体类的信息(称为Virtual Method Table,简称V-Table),其中就包含了它override的函数指针,也就是各自的draw函数。下面调用的代码就是通过查询V-Table才找到正确的实现的。

Paste_Image.png

上面的代码把class改为了protocolstruct
像前一个例子中那样,class类型的数组的每个成员只占用一个�指针的size,因为只需放入一个指针。而这个例子中,由于struct是值类型,它被放进数组时应该是拷贝进去的,所以理论上,它要占用struct自身size的内存。那不同size的struct如何被放进同一个数组呢?
当需要用protocol类型指针来�指向struct时,Swift采用了一个叫Existential Container的结构来保存�struct的成员变量和方法。如下图:

Paste_Image.png

Existential Container的前3个word称为value buffer,是用来保存struct的数据成员的。对于比较小的struct可以直接把值塞进去,对于超过3个wordstruct,则只能分配在堆上,然后在这里保存一个指针。此时每个struct在栈上占用的空间就一样了(5个word),这就解答了上面的问题。接下来,为了统一处理不同size的struct,又在第4个word增加了一个叫Value Witness Table(简称VWT)的结构,里面包含了一组函数。如下图:

Paste_Image.png

PointLine为例:

函数 Point Line
allocate 没动作 在堆上分配内存,并保存指针到value buffer
copy 把值拷到value buffer 把值拷到value buffer中的指针对应的堆内存上
destruct 如果包含引用类型的成员变量,这里需要�引用减1。此处没有,所以没动作 也没动作
deallocate 没动作 释放堆上的内存

注:实际上不止这几个函数,还有allocateBufferAndCopyValueprojectBufferdestructAndDeallocateBuffer等。

Paste_Image.png

在第5个word上添加一个Protocol Witness Table(简称PWT)的结构。里面包含protocol的成员方法的指针。PWT与V-Table很类似,程序也是通过查询这个表来实现多态的。

Paste_Image.png

上图中,左上角是源代码,左下角是编译时产生的代码。可以看到:

  • 类型为Drawable的参数编译后变成ExistContDrawable,也就是上面提到的Existential Container。
  • 函数首先在创建一个ExistContDrawable类型的临时变量,用来放参数的值。
  • 拷贝type成员(里面包含实现类的信息,图中写错了,应该是 local.type = val.type)
  • 拷贝pwt成员(里面包含struct实现的protocol中的方法的函数地址)
  • 分配空间并赋值
  • 调用projectBuffer取出数据正确的内存地址(里面判断是否需要堆操作。我想应该是从前面的type里面取出实现类的size来判断的。)
  • 调用draw函数。声明中的draw方法虽然没有参数,编译出来后会加上一个参数,就是结构体的实际内存地址。
  • 最后调用destructAndDeallocateBuffer清理内存(temp写错了,应该是local)

一个简单的调用实际做了这么多事情。这些代价都是花在需要动态判断具体struct的信息和跳转到�对应的方法上的。如果改成使用泛型,则编译器就可以在编译期知道具体类型了,也就可以进行诸如inline等优化手段。具体实现�参考下面两张图:

用`protocol`实现
用泛型实现

但是,这样做的前提是不需要使用多态。如果像上面的例子那样,需要把不同的实现类放进一个数组中,则必须借用多态了。

使用“写时拷贝”

由于struct是值类型,当它在传递时会发生多次拷贝。如果你的struct拷贝成本很高或者拷贝发生得很频繁,而修改却很少的话,可以考虑使用“写时拷贝”的方法来优化它,如下:

Paste_Image.png

此处使用一个storageclass来包装数据。当Line发生拷贝时,storage成员只发生引用计数加一的操作。当需要真正写入时,再调用isUniquelyReferencedNonObjc判断一下storage的引用计数是否大于1, 是的话则显式拷贝一份再进行写入。
内置类型String,Array,Set,Dictionary均使用了这个技术。

相关视频

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

推荐阅读更多精彩内容