真实世界中的 Swift 性能优化
那么有什么因素会导致代码运行缓慢呢?当您在编写代码并选择架构的时候,深刻认识到这些架构所带来的影响是非常重要的。我将首先谈一谈:如何理解内联、动态调度与静态调度之间的权衡,以及相关结构是如何分配内存的,还有怎样选择最适合的架构。
内存分配 (1:02)
对象的内存分配 (allocation) 和内存释放 (deallocation) 是代码中最大的开销之一,同时通常也是不可避免的。Swift 会自行分配和释放内存,此外它存在两种类型的分配方式。
第一个是基于栈 (stack-based) 的内存分配。Swift 会尽可能选择在栈上分配内存。栈是一种非常简单的数据结构;数据从栈的底部推入 (push),从栈的顶部弹出 (pop)。由于我们只能够修改栈的末端,因此我们可以通过维护一个指向栈末端的指针来实现这种数据结构,并且在其中进行内存的分配和释放只需要重新分配该整数即可。
第二个是基于堆 (heap-based) 的内存分配。这使得内存分配将具备更加动态的生命周期,但是这需要更为复杂的数据结构。要在堆上进行内存分配的话,您需要锁定堆当中的一个空闲块 (free block),其大小能够容纳您的对象。因此,我们需要找到未使用的块,然后在其中分配内存。当我们需要释放内存的时候,我们就必须搜索何处能够重新插入该内存块。这个操作很缓慢。主要是为了线程安全,我们必须要对这些东西进行锁定和同步。
引用计数 (2:30)
我们还有引用计数 (reference counting) 的概念,这个操作相对不怎么耗费性能,但是由于使用次数很多,因此它带来的性能影响仍然是很大的。引用计数是 Objective-C 和 Swift 中用于确定何时该释放对象的安全机制。目前,Swift 当中的引用计数是强制自动管理的,这意味着它很容易被开发者们所忽略。然而,当您打开 Instrument 查看何处影响了代码运行的速度的时候,您会发现 20,000 多次的 Swift 持有 (retain) 和释放 (release),这些操作占用了 90% 的代码运行时间!
Receive news and updates from Realm straight to your inbox
func perform(with object: Object) {
object.doAThing()
}
这是因为如果有这样一个函数接收了一个对象作为参数,并且执行了这个对象的 doAThing()
方法,编译器会自动插入对象持有和释放操作,以确保在这个方法的生命周期当中,这个对象不会被回收掉。
func perform(with object: Object) {
__swift_retain(object)
object.doAThing()
__swift_release(object)
}
这些对象持有和释放操作是原子操作 (atomic operations),所以它们运转缓慢就很正常了。或者,是因为我们不知道如何让它们能够运行得更快一些。
调度与对象 (3:28)
此外还有调度 (dispatch) 的概念。Swift 拥有三种类型的调度方式。Swift 会尽可能将函数内联 (inline),这样的话使用这个函数将不会有额外的性能开销。这个函数可以直接调用。静态调度 (static dispatch) 本质上是通过 V-table 进行的查找和跳转,这个操作会花费一纳秒的时间。然后动态调度 (dynamic dispatch) 将会花费大概五纳秒的时间,如果您只有几个这样的方法调用的话,这实际上并不会带来多大的问题,问题是当您在一个嵌套循环或者执行上千次操作当中使用了动态调度的话,那么它所带来的性能耗费将成百上千地累积起来,最终影响应用性能。
Swift 同样也有两种类型的对象。
class Index {
let section: Int
let item: Int
}
let i = Index(section: 1,
item: 1)
这是一个类,类当中的数据都会在堆上分配内存。您可以在此处看到,这里我们创建了一个名为 Index
的类。其中包含了两个属性,一个 section
和一个 item
。当我们创建了这个对象的时候,堆上便创建了一个指向此 Index
的指针,因此在堆上便存放了这个 section
和 item
的数据和空间。
如果我们对其建立引用,就会发现我们现在有两个指向堆上相同区域的指针了,它们之间是共享内存的。
class Index {
let section: Int
let item: Int
}
let i = Index(section: 1,
item: 1)
let i2 = i
这个时候,Swift 会自动插入对象持有操作。
class Index {
let section: Int
let item: Int
}
let i = Index(section: 1,
item: 1)
__swift_retain(i)
let i2 = i
结构体 (4:57)
很多人都会说:要编写性能优异的 Swift 代码,最简单的方式就是使用结构体了,结构体通常是一个很好的结构,因为结构体会存储在栈上,并且通常会使用静态调度或者内联调度。
存储在栈上的 Swift 结构体将占用三个 Word
大小。如果您的结构体当中的数据数量低于三种的话,那么结构体的值会自动在栈上内联。Word
是 CPU 当中内置整数的大小,它是 CPU 所工作的区块。
struct Index {
let section: Int
let item: Int
}
let i = Index(section: 1, item: 1)
在这里您可以看到,当我们创建这个结构体的时候,带有 section
和 item
值得 Index
结构体将会直接下放到栈当中,这个时候不会有额外的内存分配发生。那么如果我们在别处将其赋值到另一个变量的时候,会发生什么呢?
struct Index {
let section: Int
let item: Int
}
let i = Index(section: 1, item: 1)
let i2 = i
如果我们将 i
赋给 i2
,这会将我们存储在栈当中的值直接再次复制一遍,这个时候并不会出现引用的情况。这就是所谓的「值类型」。
那么如果结构体当中存放了引用类型的话又会怎样呢?持有内联指针的结构体。
struct User {
let name: String
let id: String
}
let u = User(name: "Joe", id: "1234")
当我们将其赋值给别的变量的时候,我们就持有了共享两个结构体的相同指针,因此我们必须要对这两个指针进行持有操作,而不是在对象上执行单独的持有操作。
struct User {
let name: String
let id: String
}
let u = User(name: "Joe",
id: "1234")
__swift_retain(u.name._textStorage)
__swift_retain(u.id._textStorage)
let u2 = u
如果其中包含了类的话,那么性能耗费会更大。
抽象类型 (6:59)
正如我们此前所述,Swift 提供了许多不同的抽象类型 (abstraction),从而允许我们自行决定代码该如何运行,以及决定代码的性能特性。现在我们来看一看抽象类型是如何在实际环境当中使用的。这里有一段很简单的代码:
struct Circle {
let radius: Double
let center: Point
func draw() {}
}
var circles = (1..<100_000_000).map { _ in Circle(...) }
for circle in circles {
circle.draw()
}
这里有一个带有 radius
和 center
属性的 Circle
结构体。它将占用三个 Word
大小的空间,并存储在栈上。我们创建了一亿个 Circle
,然后我们遍历这些 Circle
并调用这个函数。在我的电脑上,这段操作在发布模式下耗费了 0.3 秒的时间。那么当需求发生变更的时候,会发生什么事呢?
我们不仅需要绘圆,还需要能够处理多种类型的形状。让我们假设我们还需要绘线。我非常喜欢面向协议编程,因为它允许我在不使用继承的情况下实现多态性,并且它允许我们只需要考虑这个「抽象类型」即可。
protocol Drawable {
func draw()
}
struct Circle: Drawable {
let radius: Double
let center: Point
func draw() {}
}
let drawables: [Drawable] = (1..<100_000_000).map { _ in Circle(...) }
for drawable in drawables {
drawable.draw()
}
我们需要做的,就是将这个 draw
方法析取到协议当中,然后将数组的引用类型变更为这个协议,这样做导致这段代码花费了 4.0 秒的时间来运行。速率减慢了 1300%,这是为什么呢?
这是因为此前的代码可以被静态调度,从而在没有任何堆应用建立的情况下仍能够执行。这就是协议是如何实现的。
例如,如大家所见,这里是我们此前的 Circle
结构体。在这个 for 循环当中,Swift 编译器所做的就是前往 V-table 进行查找,或者直接将 draw
函数内联。
struct Circle {
let radius: Double
let center: Point
func draw() {}
}
var circles = (1..<100_000_000).map { _ in Circle(...) }
for circle in circles {
circle.draw()
}
当我们用协议来替代的时候,此时它并不知道这个对象是结构体还是类。因为这里可能是任何一个实现此协议的类型。
protocol Drawable {
func draw()
}
struct Circle: Drawable {
let radius: Double
let center: Point
func draw() {}
}
var drawables: [Drawable] = (1..<100_000_000).map { _ in return Circle(...) }
for drawable in drawables {
drawable.draw()
}
那么我们该如何去调度这个 draw
函数呢?答案就位于协议记录表 (protocol witness table,也称为虚函数表) 当中。它其中存放了您应用当中每个实现协议的对象名,并且在底层实现当中,这个表本质上充当了这些类型的别名。
protocol Drawable {
func draw()
}
struct Circle: Drawable {
let radius: Double
let center: Point
func draw() {}
}
var drawables: [Drawable] = (1..<100_000_000).map { _ in
return Circle(...)
}
for drawable in drawables {
drawable.draw()
}
在这里的代码当中,我们该如何获取协议记录表呢?答案就是从这个既有容器 (existential container) 当中获取,这个容器目前拥有一个三个字大小的结构体,并且存放在其内部的值缓冲区当中,此外还与协议记录表建立了引用关系。
struct Circle: Drawable {
let radius: Double
let center: Point
func draw() {}
}
这里 Circle
类型存放在了三个字大小的缓冲区当中,并且不会被单独引用。
struct Line: Drawable {
let origin: Point
let end: Point
func draw() {}
}
举个例子,对于我们的 Line
类型来说,它其中包含了四个字的存储空间,因为它拥有两个点类型。这个 Line
结构体需要超过四个字以上的存储空间。我们该如何处理它呢?这会对性能有影响么?好吧,它的确会:
protocol Drawable {
func draw()
}
struct Line: Drawable {
let origin: Point
let end: Point
func draw() {}
}
let drawables: [Drawable] = (1..<100_000_000).map { _ in Line(...) }
for drawable in drawables {
drawable.draw()
}
这需要花费 45 秒钟的时间来运行。为什么这里要花这么久的时间呢,发生了什么事呢?
绝大部分的时间都花费在对结构体进行内存分配上了,因为现在它们无法存放在只有三个字大小的缓冲区当中了。因此这些结构会在堆上进行内存分配,此外这也与协议有一点关系。由于既有容器只能够存储三个字大小的结构体,或者也可以与对象建立引用关系,我们同样需要某种名为值记录表 (value witness table)。这就是我们用来处理任意值的东西。
因此在这里,编译器将创建一个值记录表,对每个���缓冲区、内敛结构体来说,都有三个字大小的缓冲区,然后它将负责对值或者类进行内存分配、拷贝、销毁和内存释放等操作。
func draw(drawable: Drawable) {
drawable.draw()
}
let value: Drawable = Line()
draw(local: value)
// Generates
func draw(value: ECTDrawable) {
var drawable: ECTDrawable = ECTDrawable()
let vwt = value.vwt
let pwt = value.pwt
drawable.vwt = value.vwt
drawable.pwt = value.pwt
vwt.allocateBuffAndCopyValue(&drawable, value)
pwt.draw(vwt.projectBuffer(&drawable)
}
这里是一个例子,这就是这个过程的中间产物。如果我们只有一个 draw
函数,那么它将会接受我们创建的 Line
作为参数,因此我们将它传递给这个 draw
函数即可。
实际情况时,它将这个 Drawable
协议传递到既有容器当中,然后在函数内部再次进行创建。这会对值和协议记录表进行赋值,然后分配一个新的缓冲区,然后将其他结构、类或者类似对象的值拷贝进这个缓冲区当中。然后就使用协议记录表当中的 draw
函数,把真实的 Drable
对象传递给这个函数。
您可以看到,值记录表和协议记录表将会存放在栈上,而 Line
将会被存放在堆上,从而最后将线绘制出来。
https://academy.realm.io/cn/posts/real-world-swift-performance/
真实世界中的 Swift 性能优化的更多相关文章
- 【基本功】深入剖析Swift性能优化
简介 2014年,苹果公司在WWDC上发布Swift这一新的编程语言.经过几年的发展,Swift已经成为iOS开发语言的“中流砥柱”,Swift提供了非常灵活的高级别特性,例如协议.闭包.泛型等,并且 ...
- 深入剖析Swift性能优化
简介 2014年,苹果公司在WWDC上发布Swift这一新的编程语言.经过几年的发展,Swift已经成为iOS开发语言的“中流砥柱”,Swift提供了非常灵活的高级别特性,例如协议.闭包.泛型等,并且 ...
- vue中关于v-for性能优化---track-by属性
vue中关于v-for性能优化---track-by属性 最近看了一些react,angular,Vue三者的对比文章,对比来说Vue比较突出的是轻量级与易上手. 对比Vue与angular,Vue有 ...
- Java生鲜电商平台-SpringCloud微服务架构中网络请求性能优化与源码解析
Java生鲜电商平台-SpringCloud微服务架构中网络请求性能优化与源码解析 说明:Java生鲜电商平台中,由于服务进行了拆分,很多的业务服务导致了请求的网络延迟与性能消耗,对应的这些问题,我们 ...
- 页面中的CSS性能优化
大型网站中会有多个CSS文件,性能优化是不要的.主要有以下几个方法: 一:压缩样式表: 通过构建工具压缩CSS文件,能够减少文件的大小,从而得到更快的下载.解析和执行.对于使用预处理器例如 Sass, ...
- input屏蔽历史记录 ;function($,undefined) 前面的分号是什么用处 JSON 和 JSONP 两兄弟 document.body.scrollTop与document.documentElement.scrollTop兼容 URL中的# 网站性能优化 前端必知的ajax 简单理解同步与异步 那些年,我们被耍过的bug——has
input屏蔽历史记录 设置input的扩展属性autocomplete 为off即可 ;function($,undefined) 前面的分号是什么用处 ;(function($){$.ex ...
- Unity 3D中C#的性能优化小陷阱
本篇内容主要来自Unity官方手册: 一般性能优化 一些地方为本人瞎编杜撰,请酌情参考.如有错误,欢迎指出. Unity里C#编程虽然既简单还很爽,但是性能小陷阱还不少.我总强迫自己让代码最优,因此很 ...
- 关于Net开发中一些SQLServer性能优化的建议
一. ExecuteNonQuery和ExecuteScalar 对数据的更新不需要返回结果集,建议使用ExecuteNonQuery.由于不返回结果集可省掉网络数据传输.它仅仅返回受影响的行数.如果 ...
- C#中 EF(EntityFramework) 性能优化
现在工作中很少使用原生的sql了,大多数的时候都在使用EF.刚开始的时候,只是在注重功能的实现,最近一段时间在做服务端接口开发.开发的时候也是像之前一样,键盘噼里啪啦的一顿敲,接口秒秒钟上线,但是到联 ...
随机推荐
- vue-cli 中遇见的问题,记录爬坑日常!
本片文章我将会记录使用vue-cli 以及一些相关插件遇见的问题和解决方案,另外本文章将会持续更新,本着互联网分享精神,希望我所记录的日常能对大家有所帮助. 1.在img和html文件处于同级阶段,i ...
- 阿里云服务器(Ubuntu16.04 64位)远程连接
购买阿里云服务器 1.打开阿里云官方网站,账号登录,选择产品中的云服务器 ECS 2.根据自身需求,选择合适的阿里云服务器系统,(1)点击一键购买,(2)选择地域,(3)根据自身需求,选择系统,这里选 ...
- Bash on windows从14.0升级到ubuntu16.04
升级参考:https://www.zhihu.com/question/49411626 解决中文乱码问题参考:http://www.lofter.com/tag/ubuntu%E5%AD%90%E7 ...
- 【说文解字】Unix与Linux
历史 Unix操作系统是由Ken Thompson和Dennis Ritchie于1969-1970年发明. 它的部分技术来源可以追溯到Multics工程,后者因为过于庞大复杂而失败. 研究人员吸取教 ...
- C#3.0匿名类和Lanmda表达式
1.初始化器:className variableName = new className(property1=value1…); 2.var可以声明一个没有类型的变量,根据赋值的不同改变数据类型 3 ...
- Node.js如何找npm模板
首先需要去官网下载npm文件 https://www.npmjs.com/ 下载完成,使用CD查看是否安装完成 然后就是贴代码看npm模板的功能 var _ = require('underscore ...
- 第二天-while循环 格式化输出 运算符 编码
一.while循环 while 条件: 语句块(循环体) #判断条件是否成立,若成立执行循环体,然后再次判断条件...直到不满足跳出循环 else: 当条件不成立的时候执行这里,和break没 ...
- 多项式乘法,FFT与NTT
多项式: 多项式?不会 多项式加法: 同类项系数相加: 多项式乘法: A*B=C $A=a_0x^0+a_1x^1+a_2x^2+...+a_ix^i+...+a_{n-1}x^{n-1}$ $B=b ...
- jQuery全能图片滚动插件
插件开发背景 随着前端开发领域越来越受到重视,前端开发也变得越来越火热.各种优秀的前端组件层出不穷.尤其是jQuery插件,很多前端组件都是基于jQuery开开发的. 图片滚动是前端开发中可以说是非常 ...
- 02.php面向对象——构造方法&析构方法
<?php //自己写的构造方法 class Computer{ public function Computer(){ echo '构造方法'; } } new Computer();//这样 ...