Swift 性能相关

起初的疑问源自于「在 Swift 中的, Struct:Protocol 比 抽象类 好在哪里?」。但是找来找去都是 Swift 性能相关的东西。整理了点笔记,供大家可以参考一下。

一些疑问

在正题开始之前,不知道你是否有如下的疑问:

  • 为什么说 Swift 相比较于 Objective-C 会更加 快 ?
  • 为什么在编译 Swift 的时候这么 慢 ?
  • 如何更 优雅 的去写 Swift ?

如果你也有类似疑问,希望这篇笔记能帮你解释一下上面几个问题的一些原因。(ps.上面几个问题都很大,如果有不同的想法和了解,也希望你能分享出来,大家一起讨论一下。)

Swift中的类型

首先,我们先统一一下关于类型的几个概念。

  • 平凡类型

有些类型只需要按照字节表示进行操作,而不需要额外工作,我们将这种类型叫做平凡类型 (trivial)。比如,Int 和 Float 就是平凡类型,那些只包含平凡值的 struct 或者 enum 也是平凡类型。

 
  • 引用类型

对于引用类型,值实例是一个对某个对象的引用。复制这个值实例意味着创建一个新的引用,这将使引用计数增加。销毁这个值实例意味着销毁一个引用,这会使引用计数减少。不断减少引用计数,最后当然它会变成 0,并导致对象被销毁。但是需要特别注意的是,我们这里谈到的复制和销毁值,只是对引用计数的操作,而不是复制或者销毁对象本身。

 
  • 组合类型

类似 AClass 这类,引用类型包含平凡类型的,其实还是引用类型,但是对于平凡类型包含引用类型,我们暂且称之为组合类型。

 

影响性能的主要因素

主要原因在下面几个方面:

  • 内存分配 ( Allocation ):主要在于 堆内存分配 还是 栈内存分配 。
  • 引用计数 ( Reference counting ):主要在于如何 权衡 引用计数。
  • 方法调度 ( Method dispatch ):主要在于 静态调度 和 动态调度 的问题。

内存分配(Allocation)

今天主要谈一谈 内存分区 中的 堆 和 栈 。

  • 堆( heap ) 

堆是用于存放进程运行中被 动态分配的内存段 ,它的大小并不固定,可动态扩张或 缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张); 当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)

  • 栈 ( stack heap ) 

栈又称堆栈, 是 用户存放程序临时创建的局部变量 ,也就是说我们函数括弧“{}” 中定义的变量(但不包括static声明的变量,static意味着在 数据段 中存放变量)。除此以外, 在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值 也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

在 Swift 中,对于 平凡类型 来说都是存在 栈 中的,而 引用类型 则是存在于 堆 中的,如下图所示:

Swift 性能相关

我们都知道,Swift建议我们多用 平凡类型 ,那么 平凡类型 比 引用类型 好在哪呢?换句话说「在 栈 中的数据和 堆中的数据相比有什么优势?」

  • 数据结构
    • 存放在栈中的数据结构较为简单,只有一些值相关的东西
    • 存放在堆中的数据较为复杂,如上图所示,会有type、retainCount等。
  • 数据的分配与读取
    • 存放在栈中的数据从栈区底部推入 (push),从栈区顶部弹出 (pop),类似一个数据结构中的栈。由于我们只能够修改栈的末端,因此我们可以通过维护一个指向栈末端的指针来实现这种数据结构,并且在其中进行内存的分配和释放只需要重新分配该整数即可。所以栈上分配和释放内存的代价是很小。
    • 存放在堆中的数据并不是直接 push/pop,类似数据结构中的链表,需要通过一定的算法找出最优的未使用的内存块,再存放数据。同时销毁内存时也需要重新插值。
  • 多线程处理
    • 栈是线程独有的,因此不需要考虑线程安全问题。
    • 堆中的数据是多线程共享的,所以为了防止线程不安全,需同步锁来解决这个问题题。

综上几点,在内存分配的时候,尽可能选择 栈 而不是 堆 会让程序运行起来更加快。

引用计数(Reference counting)

首先 引用计数 是一种 内存管理技术 ,不需要程序员直接去操作指针来管理内存。

而采用 引用计数 的 内存管理技术 ,会带来一些性能上的影响。主要以下两个方面:

  • 需要通过大量的 release/retain 代码去维护一个对象生命周期。
  • 存放在 堆区 的是多线程共享的,所以对于 retainCount 的每一次修改都需要通过同步锁等来保证线程安全。

对于 自动引用计数 来说, 在添加 release/retain 的时候采用的是一个宁可多写也不漏写的原则,所以 release/retain 有一定的冗余。这个冗余量大概在 10% 的左右(如下图,图片来自于 iOS可执行文件瘦身方法 )。

Swift 性能相关

而这也是为什么虽然 ARC 底层对于内存管理的算法进行了优化,在速度上也并没有比 MRC 写出来的快的原因。 这篇文章 详细描述了 ARC 和 MRC 在速度上的比较。

综上,虽然因为自动引用计数的引入,大大减少了内存管理相关的事情,但是对于引用计数来说,过多或者冗余的引用计数是会减慢程序的运行的。

而对于引用计数来说,还有一个 权衡问题 ,具体如何权衡会再后文解释。

方法调度 (Method dispatch)

在 Swift 中, 方法的调度主要分为两种:

  • 静态调度 : 可以进行inline和其他编译期优化,在执行的时候,会直接跳到方法的实现。
 
  • 动态调度 : 在执行的时候,会根据运行时,采用 V-Table 的方式,找到方法的执行体,然后执行。无法进行编译期优化。 V-Table 不同于 OC 的调度,在 OC 中,是先在运行时的时候先在子类中寻找方法,如果找不到,再去父类寻找方法。而对于 V-Table 来说,它的调度过程如下图:

Swift 性能相关

因此,在性能上「 静态调度 > 动态调度 」并且「 Swift中的V-Table > Objective-C 的动态调度 」。

协议类型 (Protocol types)

在 Swift 引入了一个 协议类型 的概念,示例如下:

 

在上述代码中, Drawable 就称为协议类型,由于 平凡类型 没有继承,所以实现多态上出现了一些棘手的问题,但是 Swift 引入了 协议类型 很好的解决了 平凡类型 多态的问题,但是在设计 协议类型 的时候有两个最主要的问题:

  • 对于类似 Drawable 的协议类型来说,如何去调度一个方法?
  • 对于不同的类型,具有不同的size,当保存到 drawables 数组时,如何保证内存对齐?

对于第一个问题,如何去调度一个方法?因为对于 平凡类型 来说,并没有什么虚函数指针,所以在 Swift 中并没有 V-Table 的方式,但是还是用到了一个叫做 The Protocol Witness Table (PWT) 的函数表,如下图所示:

Swift 性能相关

对于每一个 Struct:Protocol 都会生成一个 StructProtocol 的 PWT 。

对于第二个问题,如何保证内存对齐问题?

Swift 性能相关

有一个简单粗暴的方式就是,取最大的Size作为数组的内存对齐的标准,但是这样一来不但会造成内存浪费的问题,还会有一个更棘手的问题,如何去寻找最大的Size。所以为了解决这个问题,Swift 引入一个叫做 Existential Container 的数据结构。

Swift 性能相关

  • Existential Container

Swift 性能相关

这是一个最普通的 Existential Container。

  • 前三个word:Value buffer。用来存储Inline的值,如果word数大于3,则采用指针的方式,在堆上分配对应需要大小的内存
  • 第四个word:Value Witness Table(VWT)。每个类型都对应这样一个表,用来存储值的创建,释放,拷贝等操作函数。(管理 Existential Container 生命周期)
  • 第五个word:Protocol Witness Table(PWT),用来存储协议的函数。

用伪代码表示如下:

 

所以,对于上文代码中的 Point 和 Line 最后的数据结构大致如下:

Swift 性能相关

这里需要注意的几个点:

  • 在 ABI 稳定之前 value buffer 的 size 可能会变,对于是不是 3个 word 还在 Swift 团队还在权衡.
  • Existential Container 的 size 不是只有 5 个 word。示例如下:

Swift 性能相关

对于这个大小差异最主要在于这个 PWT 指针,对于 Any 来说,没有具体的函数实现,所以不需要 PWT 这个指针,但是对于 ProtocolOne&ProtocolTwo 的组合协议,是需要两个 PWT 指针来表示的。

OK,由于 Existential Container 的引入,我们可以将协议作为类型来解决 平凡类型 没有继承的问题,所以 Struct:Protocol 和 抽象类就越来越像了。

回到我们最初的疑问,「在 Swift 中的, Struct:Protocol 比 抽象类 好在哪里?」

  • 由于 Swift 只能是单继承,所以 抽象类 很容易造成 「上帝类」 ,而Protocol可以是一个多这多个则没有这个问题
  • 在内存分配上上,Struct是在栈中的,而抽象类是在堆中的,所以 简单数据 的Struct:Protocol会再性能上比抽象类更加好
  • (写起来更加有逼格算不算?)

但是,虽然表面上协议类型确实比抽象类更加的 “好” ,但是我还是想说,不要随随便便把协议当做类型来使用。

为什么这么说?先来看一段代码:

 

首先,我们把 Drawable 协议当做一个类型,作为 Pair 的属性,由于协议类型的 value buffer 只有三个 word,所以如果一个 struct(比如上文的Line) 超过三个 word,那么会将值保存到堆中,因此会造成下图的现象:

Swift 性能相关

一个简单的复制,导致属性的copy,从而引起 大量的堆内存分配 。

所以,不要随随便便把协议当做类型来使用。上面的情况发生于无形之中,你却没有发现。

当然,如果你非要将协议当做类型也是可以解决的,首先需要把Line改为class而不是struct,目的就是引入引用计数。所以,将Line改为class之后,就变成了如下图所示:

Swift 性能相关

至于修改了 line 的 x1 导致所有 pair 下的 line 的 x1 的值都变了,我们可以引入 Copy On Write 来解决。

当我们 Line 使用平凡类型时,由于line占用了4个word,当把协议作为类型时,无法将line存在 value buffer 中,导致了堆内存分配,同时每一次复制都会引发堆内存分配,所以我们采用了引用类型来替代平凡类型,增加了引用计数而降低了堆内存分配,这就是一个很好的引用计数权衡的问题。

泛型(Generic code)

首先,如果我们把协议当做类型来处理,我们称之为 「动态多态」 ,代码如下:

 

而如果我们使用泛型来改写的话,我们称之为 「静态多态」 ,代码如下:

 

而这里所谓的 动态 和 静态 的区别在哪里呢?

在 Xcode 8 之前,唯一的区别就是由于使用了泛型,所以在调度方法是,我们已经可以根据上下文确定了这个 T 到底是什么类型,所以并不需要 Existential Container ,所以泛型没有使用 Existential Container ,但是因为还是多态,所以还是需要VWT和PWT作为隐形参数传递,对于临时变量仍然按照ValueBuffer的逻辑存储 - 分配3个word,如果存储数据大小超过3个word,则在堆上开辟内存存储。如图所示:

Swift 性能相关

这样的形式其实和把协议作为类型并没有什么区别。唯一的就是没有 Existential Container 的中间层了。

但是,在 Xcode 8 之后,引入了 Whole-Module Optimization 使泛型的写法更加静态化。

首先,由于可以根据上下文知道确定的类型,所以编译器会为每一个类型都生成一个drawACopy的方法,示例如下:

 

由于每个类型都生成了一个drawACopy的方法,drawACopyOfAPoint的调用就吧编程了一个静态调度,再根据前文静态调度的时候,编译器会做 inline 处理,所以上面的代码经过编译器处理之后代码如下:

 

由于编译器一步步的处理,再也不需要 vwt、pwt及value buffer了。所以对于泛型来做多态来说,就叫做静态多态。

几点总结

  • 为什么在编译 Swift 的时候这么慢
    • 因为编译做了很多事情,例如 静态调度的inline处理,静态多态的分析处理等
  • 为什么说 Swift 相比较于 Objective-C 会更加快
    • 对于Swift来说,更多的静态的,比如静态调度、静态多态等。
    • 更多的栈内存分配
    • 更少的引用计数
  • 如何更优雅的去写 Swift
    • 不要把协议当做类型来处理
    • 如果需要把协议当做类型来处理的时候,需要注意 big Value 的复制就引起堆内存分配的问题。可以用 Indirect Storage + Copy On Write 来处理。
    • 对于一些抽象,可以采用 Struct:Protocol 来代替抽象类。至少不会有 上帝类 出现,而且处理的好的话性能是比抽象类更好的。

参考资料

  • Understanding Swift Performance
  • 真实世界中的 Swift 性能优化
  • Exploring Swift Memory Layout
  • 水平有限,若有错误,希望多多指正!coderonevv#gmail.com

更多

工作之余,写了点笔记,如果需要可以在我的 GitHub 看。

查看原文: Swift 性能相关

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/253471.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTTPS 路径配置

1: 首先安装 fiddlercertmaker.exe 文件2:Tools -> HTTPS 3: Connections 勾中Allow remote computer to connect转载于:https://www.cnblogs.com/eason-d/p/7492177.html

CMOS图像传感器——相位对焦

之前介绍了许多自动对焦的方案 自动对焦方法学习_沧海一升的博客-CSDN博客自动对焦的各类方法学习介绍https://blog.csdn.net/qq_21842097/article/details/121373263 在里面提到了遮蔽像素相位检测法,原理上算是相位检测法(Phase Detection Auto Focus,PDAF)的一种。…

Spring Cloud Config 和Spring Cloud Bus实现配置中心

2019独角兽企业重金招聘Python工程师标准>>> Spring Cloud是很多组件的集合,Spring将常用的技术框架进行包装和整合,如mybatis zookeeper rabbitmq redis等等,还有一些科技公司贡献出来的一些经过生产环境验证的组件如奈飞公司贡献…

CMOS图像传感器——闪烁(flicker)现象

一、概述 闪烁(Flicker),通常发生在室内场景,曝光时间设置如果不是光源能量周期的整数倍,则图像不同位置处积累的信号强度不同,并呈周期性变化,这是单帧图像的情况。在视频序列上,如果满足一定条件,视频会出现条纹模式在垂直方向上缓慢移动。 二、形成原因 1、光源 …

CMOS图像传感器——图像传感器噪声

图像传感器噪声取决于图像传感器的制作工艺、内部结构及内部补偿技术等原因,噪声反应了图像传感器的内部特性。CMOS图像传感器基本原理见: CMOS图像传感——概述_沧海一升的博客-CSDN博客_cmos图像传感器CMOS图像传感器基本介绍https://blog.csdn.net/qq_21842097/article/d…

TI Davinci DM6441嵌入式Linux移植攻略——UBL移植篇

目录(?)[] 一DM6441的Boot过程简介二DM6441的UBL移植 CCS文件夹Common文件夹GNU文件夹 移植DDR2移植Nand Flash其它 声明:本文参考网友zjb_integrated的文章《TI Davinci DM6446开发攻略——UBL移植》和《DAVINCI DM365-DM368开发攻略——U-BOOT-2010.12及UBL的移…

python接口自动化测试(二)-requests.get()

环境搭建好后,接下来我们先来了解一下requests的一些简单使用,主要包括: requests常用请求方法使用,包括:get,postrequests库中的Session、Cookie的使用其它高级部分:认证、代理、证书验证、超时…

数字图像处理——图像锐化

图像增强是图像处理的一个重要环节,早期的图像处理就是从图像增强开始的,人们研究对质量低的图像进行处理以获得改善质量后的图像。现今的图像增强还为后续的图像处理,如图像信息提取、图像识别等,提供更高识别度的图像。 从图像处理技术来看,图像的摄取、编码、传输和处理…

DAVINCI DM365-DM368开发攻略——U-BOOT-2010.12及UBL的移植

从盛夏走到深秋,我们继续DAVINCI DM365-DM368的开发。说来惭愧,人家51CTO热情支持本博客,而本人却一直没有像其他博客之星一样频繁更新博客,心里确实说不过去。管理公司确实很累,有更急的客户的项目要做,我…

SerDes接口——架构与电路

随着通信技术的飞速发展,高速串行互连以其结构简单,不需要传输同步时钟,相比并行传输有更高数据传输效率的优点,成为现代通信和数据传输的重要组成部分。随着对数据传输速率要求的不断提高,SERDES应运而生。它是一种时…

Springboot分模块开发详解(2):建立子工程

1.创建base-entity 选中base工程&#xff0c;右键创建一个新的maven工程 自动选择了base这个目录存放子工程 创建后&#xff0c;pom.xml修改成如下内容&#xff1a; <?xml version"1.0"?> <projectxsi:schemaLocation"http://maven.apache.org/POM/4…

图像去雾算法学习

现有的图像采集设备对外界环境的干扰非常敏感,在雾霾环境中,获取的户外图像往往退化严重,主要表现为场景特征信息模糊、对比度低、色彩失真,不利于计算机视觉系统对图像真实特征的提取,从而影响其后续的分析、理解、识别等一系列处理,很大程度上降低了视觉系统的实际应用…

训练与解码

BW算法是对某一个HMM(一个音素)进行训练&#xff0c;需要该HMM对应的观察向量(一段音频)&#xff0c;如何让一段文本中的某个音素找到对应一整段音频中的一小段音频&#xff1f;需要用到对齐来找到所有的[音素-音频]的配对。 训练时也需要解码 1&#xff0c;设训练的一句话有n…

CMOS 图像传感器——Color Filter Array

在介绍CMOS图像传感器的工作原理时候说道,像点(Sensor感光的基本单元叫做“像点”)吸收入射光后会有一定概率激发出电子,这个过程叫做光电转换。光子激发出电子会被像点下方的电场捕获并存储起来备用。像点的作用可以类比成一个盛水的小桶,它可以在一定范围内记录其捕获的…

我的一点企业做云经验

最近&#xff0c;经常有朋友问我在企业做云的经验&#xff0c;也有人问我OpenStack二次开发项目经验。正好这方面也有点经历&#xff0c;那现在就把我过往有关经历整理整理&#xff0c;总结出几条心得体会&#xff0c;分享给大家。 技术&#xff1a;我们OpenStack二次开发做了什…

【leetcode】910. Smallest Range II

题目如下&#xff1a; 解题思路&#xff1a;我的思路是先找出最大值。对于数组中任意一个元素A[i]来说&#xff0c;如果A[i] K 是B中的最大值&#xff0c;那么意味着从A[i1]开始的元素都要减去K&#xff0c;即如果有A[i] K > A[-1] - K&#xff0c;那么A[i] K 就可以作为…

CMOS图像传感器架构的演变

01、 引言 图像传感器目前用于多种应用。自 1969 年电荷耦合器件 (CCD) 发明以来&#xff0c;固态图像传感器已蔓延到各种消费市场&#xff0c;例如小型摄像机和数码相机。自 2005年以来已成为主流固态图像传感器的 CMOS 图像传感器在为 CCD 开发的技术的基础上不断发展。除了…

Python判断变量的数据类型的两种方法

2019独角兽企业重金招聘Python工程师标准>>> 1、isinstance(变量名&#xff0c;类型) def varargsql(self, sql, *args):if isinstance(args, tuple):self.cursor.execute(sql, args)self.conn.commit() 2、通过与其他已知类型的常量进行对比&#xff08;type()&…

基于事件的视觉传感器

在之前的文章里 人工智能与图像传感器_沧海一升的博客-CSDN博客_人工智能和传感器的关系第一类是图像传感器与人工智能计算相结合,即图像传感器模组除了可以输出图像之外,还可以直接输出人工智能算法计算的结果。另一类智能图像传感器则是为人工智能应用专门设计的图像传感器…

RocketMQ多Master多Slave模式部署

每个 Master 配置一个 Slave&#xff0c;有多对Master-Slave&#xff0c;HA采用同步双写方式&#xff0c;主备都写成功&#xff0c;向应用返回成功。 优点&#xff1a;数据与服务都无单点&#xff0c;Master宕机情况下&#xff0c;消息无延迟&#xff0c;服务可用性与数据可用性…