Swift进阶之内存模型和方法调度

前言

Apple今年推出了Swift3.0,较2.3来说,3.0是一次重大的升级。关于这次更新,在这里都可以找到,最主要的还是提高了Swift性能,优化了Swift API的设计(命名)规范

前段时间对之前写的一个项目ImageMaskTransition做了简单迁移,先保证能在3.0下正常运行,只用了不到30分钟。总的来说,这次迁移还是非常轻松的。但是,有一点要注意:3.0的API设计规范较2.3有了质变,建议做迁移的开发者先看下WWDC的Swift API Design Guidelines。后面有时间了,我有可能也会总结下。

内存分配

通过查看Github上Swift的源代码语言分布

可以看到

  • Swift语言是用C++写的
  • Swift的核心Library是用Swift自身写的。

对于C++来说,内存区间如下

  • 堆区
  • 栈区
  • 代码
  • 全局静态区

Swift的内存区间和C++类似。也有存储代码全局变量的区间,这两种区间比较简单,本文更多专注于以下两个内存区间。

  • Stack(栈),存储值类型的临时变量,函数调用栈,引用类型的临时变量指针
  • Heap(堆),存储引用类型的实例



在栈上分配和释放内存的代价是很小的,因为栈是一个简单的数据结构。通过移动栈顶的指针,就可以进行内存的创建和释放。但是,栈上创建的内存是有限的,并且往往在编译期就可以确定的。

举个很简单的例子:当一个递归函数,陷入死循环,那么最后函数调用栈会溢出。

例如,一个没有引用类型Struct的临时变量都是在栈上存储的


struct Point{
    var x:Double // 8 Bytes
    var y:Double // 8 bytes
}
let size = MemoryLayout<Point>.size
print(size) // 16
let point1 = Point(x:5.0,y:5.0)
let instanceSize = MemoryLayout<Point>.size(ofValue: point1)
print(instanceSize) //16
那么,这个内存结构如图

Tips: 图中的每一格都是一个Word大小,在64位处理器上,是8个字节

在堆上可以动态的按需分配内存,每次在堆上分配内存的时候,需要查找堆上能提供相应大小的位置,然后返回对应位置,标记指定位置大小内存被占用。

在堆上能够动态的分配所需大小的内存,但是由于每次要查找,并且要考虑到多线程之间的线程安全问题,所以性能较栈来说低很多。

比如,我们把上文的struct改成class,

class PointClass{
    var x:Double = 0.0
    var y:Double = 0.0
}
let size2 = MemoryLayout<PointClass>.size
print(size2) //8 
let point2 = Point(x:5.0,y:5.0)
let instanceSize = MemoryLayout<Point>.size(ofValue: point2)
print(instanceSize) //8
这时候的内存结构如图

Tips: 图中的每一格都是一个Word大小,在64位处理器上,是8个字节

Memory Alignment(内存对齐)

和C/C++/OC类似,Swift也有Memory Alignment的概念。举个直观的例子
我们定义这样两个Struct

struct S{
    var x:Int64
    var y:Int32
}
struct SReverse{
    var y:Int32
    var x:Int64
}

然后,用MemoryLayout来获取两个结构体的大小

let sSize = MemoryLayout<S>.size //12
let sReverseSize = MemoryLayout<SReverse>.size //16

可以看到,只不过调整了结构体中的声明顺序,其占用的内存大小就改变了,这就是内存对齐。

我们来看看,内存对齐后的内存空间分布:

内存对齐的原因是,

现代cpu每次读数据的时候,都是读取一个word(32位处理器上是4个字节,64位处理器上是8个字节)。

内存对齐的优点很多

  • 保证对一个成员的访问在一个Transition中,提高了访问速度,同时还能保证一次操作的原子性。除了这些,内存对齐还有很多优点,可以看看这个SO答案

自动引用计数(ARC)

提到ARC,不得不先讲讲Swift的两种基本类型:

  • 值类型,在赋值的时候,会进行值拷贝
  • 引用类型,在赋值的时候,只会进行引用(指针)拷贝

比如,如下代码

struct Point{ //Swift中,struct是值类型
    var x,y:Double
}
class Person{//Swift中,class是引用类型
    var name:String
    var age:Int
    init(name:String,age:Int){
        self.name = name
        self.age = age
    }
}
var point1 = Point(x: 10.0,y: 10.0)
var point2 = point1
point2.x = 9.0
print(point1.x) //10.0

var person1 = Person(name: "Leo",age: 24)
var person2 = person1
person2.age = 25
print(person1.age)//9.0
我们先看看对应内存的使用值类型有很多优点,其中主要的优点有两个 - 线程安全,每次都是获得一个copy,不存在同时修改一块内存 - 不可变状态,使用值类型,不需要考虑别处的代码可能会对当前代码有影响。也就没有side effect。ARC是相对于引用类型的。> ARC是一个内存管理机制。当一个引用类型的对象的reference count(引用计数)为0的时候,那么这个对象会被释放掉。我们利用XCode 8和iOS开发,来直观的查看下一个值类型变量的引用计数变化。新建一个iOS单页面工程,语言选择Swift,然后编写如下代码![这里写图片描述](http://img.blog.csdn.net/20161113111539024)然后,当断点停在24行处的时候,Person的引用计数如下 这里,底部的`thread_2673`是主线程堆Person对象的持有,是iOS系统添加。所以,` var leo = Person(name: “Leo”,age: 25)`这一行后,准确的说是引用计数 一,并不是引用计数 一。当然,这些系统自动创建的也会自动销毁,我们无须考虑。可以看到,person唯一的引用就是来自`VM:Stack thread`,也就是栈上。因为引用计数的存在,Class在堆上需要额外多分配一个Word来存储引用计数:


当栈上代码执行完毕,栈会断掉对Person的引用,引用计数也就减一,系统会断掉自动创建的引用。这时候,person的引用计数位0,内存释放。

方法调度(method dispatch)

Swift的方法调度分为两种

  • 静态调度 static dispatch. 静态调度在执行的时候,会直接跳到方法的实现,静态调度可以进行inline和其他编译期优化。
  • 动态调度 dynamic dispatch. 动态调度在执行的时候,会根据运行时(Runtime),采用table的方式,找到方法的执行体,然后执行。动态调度也就没有办法像静态那样,进行编译期优化。

Struct

对于Struct来说,方法调度是静态的。

struct Point{
    var x:Double // 8 Bytes
    var y:Double // 8 bytes
    func draw(){
        print("Draw point at\(x,y)")
    }
}
let point1 = Point(x: 5.0,y: 5.0)
point1.draw()
print(MemoryLayout<Point>.size) //16

可以看到,由于是Static dispatch,在编译期就能够知道方法的执行体。所以,在Runtime也就不需要额外的空间来存储方法信息。编译后,方法调用,直接就是变量地址的传入,存在了代码区中。

如果开启了编译器优化,那么上述代码被优化成Inline后,

let point1 = Point(x: 5.0,y: 5.0)
print("Draw point at\(point1.x,point1.y)")
print(MemoryLayout<Point>.size) //16

Class

Class是Dynamic dispatch的,所以在添加方法之后,Class本身在栈上分配的仍然是一个word。堆上,需要额外的一个word来存储Class的Type信息,在Class的Type信息中,存储着virtual table(V-Table)。根据V-Table就可以找到对应的方法执行体。

class Point{
    var x:Double // 8 Bytes
    var y:Double // 8 bytes
    init(x:Double,y:Double) {
        self.x = x
        self.y = y
    }
    func draw(){
        print("Draw point at\(x,y)")
    }
}
let point1 = Point(x: 5.0,y: 5.0)
point1.draw()
print(MemoryLayout<Point>.size) //8

继承

因为Class的实体会存储额外的Type信息,所以继承理解起来十分容易。子类只需要存储子类的Type信息即可。
例如

class Point{
    var x:Double // 8 Bytes
    var y:Double // 8 bytes
    init(x:Double,y:Double) {
        self.x = x
        self.y = y
    }
    func draw(){
        print("Draw point at\(x,y)")
    }
}
class Point3D:Point{
    var z:Double // 8 Bytes
    init(x:Double,y:Double,z:Double) {
        self.z = z
        super.init(x: x,y: y)
    }
    override func draw(){
        print("Draw point at\(x,y,z)")
    }
}
let point1 = Point(x: 5.0,y: 5.0)
let point2 = Point3D(x: 1.0,y: 2.0,z: 3.0)
let points = [point1,point2]
points.forEach { (p) in
    p.draw()
}
//Draw point at(5.0,5.0)
//Draw point at(1.0,2.0,3.0)

协议

我们首先看一段代码

struct Point:Drawable{
    var x:Double // 8 Bytes
    var y:Double // 8 bytes
    func draw(){
        print("Draw point at\(x,y)")
    }
}
struct Line:Drawable{
    var x1:Double // 8 Bytes
    var y1:Double // 8 bytes
    var x2:Double // 8 Bytes
    var y2:Double // 8 bytes
    func draw(){
        print("Draw line from \(x1,y1) to \(x2,y2)")
    }
}
let point = Point(x: 1.0,y: 2.0)
let memoryAsPoint = MemoryLayout<Point>.size(ofValue: point)
let memoryOfdrawable = MemoryLayout<Drawable>.size(ofValue: point)
print(memoryAsPoint)
print(memoryOfdrawable)

let line = Line(x1: 1.0,y1: 1.0,x2: 2.0,y2: 2.0)
let memoryAsLine = MemoryLayout<Line>.size(ofValue: line)
let memoryOfdrawable2 = MemoryLayout<Drawable>.size(ofValue: line)
print(memoryAsLine)
print(memoryOfdrawable2)

可以看到,输出

16 //point as Point
40 //point as Drawable
32 //line as Line
40 //line as Drawable
16和32不难理解,Point含有两个Double属性,Line含有四个Double属性。对应的字节数也是对的。那么,两个40是怎么回事呢?而且,对于Point来说,40-16=24,多出了24个字节。而对于Line来说,只多出了40-32=8个字节。这是因为Swift对于协议类型的采用如下的内存模型 - Existential Container。


Existential Container包括以下三个部分:

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

那么,内存结构图,如下



[ point ]


[ line ]

范型

范型让代码支持静态多态。比如:

func drawAcopy<T : Drawable>(local : T) {
  local.draw()
}
drawAcopy(Point(...))
drawAcopy(Line(...))
那么,范型在使用的时候,如何调用方法和存储值呢?

[ 范型 ]

范型并不采用Existential Container,但是原理类似。

  1. VWT和PWT作为隐形参数,传递到范型方法里。
  2. 临时变量仍然按照ValueBuffer的逻辑存储 - 分配3个word,如果存储数据大小超过3个word,则在堆上开辟内存存储。

范型的编译器优化

1. 为每种类合成具体的方法
比如

func drawAcopy<T : Drawable>(local : T) {
  local.draw()
}

在编译过后,实际会有两个方法

func drawAcopyOfALine(local : Line) {
  local.draw()
}
func drawAcopyOfAPoint(local : Point) {
  local.draw()
}

然后,

drawAcopy(local: Point(x: 1.0,y: 1.0))

会被编译成为

func drawAcopyOfAPoint(local : Point(x: 1.0,y: 1.0))

Swift的编译器优化还会做更多的事情,上述优化虽然代码变多,但是编译器还会对代码进行压缩。所以,实际上,并不会对二进制包大小有什么影响。

参考资料

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。