大多数浏览器和
Developer App 均支持流媒体播放。
-
了解 Swift 性能
在本次高级讲座中,了解如何在 Swift 中实施结构、类、协议和泛型。了解它们在性能的不同维度上的相对成本。了解如何运用这个信息来提升代码运行速度。
资源
相关视频
WWDC20
WWDC16
-
下载
了解Swift功能 大家好 欢迎参加“了解Swift功能”演讲 我是Kyle 今天能跟大家一起探讨Swift 我和Arnold都很激动 作为开发人员 Swift提供了广阔而 强大的设计空间等待我们去探索 Swift有各种各样的头等类型 以及各种代码复用和动态机制 语言中的所有特性都能 以一种有趣、迅捷的方式结合在一起 那么我们该如何缩小这个设计空间 并为我们的项目选择 合适的工具呢? 嗯 首先 你要考虑到 Swift的多种抽象机制的建模意义 值或引用语义是否恰当? 这个抽象需要有多动态?
嗯 今天我和Arnold也想使你们 用性能来缩小设计空间 以我的经验来说 考虑性能影响 总会有一个 更顺畅的方案
那么 我们主要关注的是性能 我们会接触点儿建模 但我们去年做了一些很不错的 演讲 今年我们也会继续努力 主要内容是在Swift中 为项目建模的强大技巧 如果你想充分利用本场演讲 我强烈建议你们至少参加 一场这类演讲 好了 那么 我们想用性能来缩小设计空间 嗯 理解 性能影响的最好方式是 Swift抽象机制的 理解它们的优先执行 这就是我们今天要讲的主题 我们从识别 开始 不同的维度 当评估不同的抽象机制选项时 其中的每一项 我们都会 演示一些代码 用结构和类 深化我们的心智模型 所涉及到的总开销 然后 看看如何应用我们所学的技术 来清理和加速一些Swift代码 在演讲的后半场 我们要评估 面向协议程序设计的性能 我们要看一下 高级Swift功能的实现 如协议和泛型 来更好地理解 它们的建模和性能影响 免责声明:我们要看一下 内存表现 和所生成代码的表现 当Swift编译和执行 你的代码时 这些必然要被简化 但我和Arnold认为 已达到了很好的平衡 在简易和准确之间 这是一个很好的推理出 代码的心智模型 好了 让我们从识别 性能的不同维度开始吧 那么 当你在创建一个抽象 并选择一个抽象机制时 你应该问问自己 我的实例要分配给堆栈还是堆? 当我传递这个实例时 我要产生多少计算开销? 当我在这个实例中调用方法时 要静态还是动态发送? 当我们想快速地写Swift代码时 我们就要避免 为我们不能利用的动态 和运行时间付出代价
我们需要学习何时以及如何在 这些不同维度之间切换 来获得更好的性能 好了 我们要讲一下每种维度 一个一个地讲 从分配开始
Swift会替你自动分配内存 并取消内存配额 有些内存会分配给堆栈 堆栈是一种非常简单的数据结构 你可以推到栈底 并弹出栈底 因为你能只添加或移除栈底 所以我们可以 实现堆栈或实现入栈和出栈 仅仅通过在栈底放一个指针
意思是 当我们调用函数时 或不如说 我们把栈底的指针叫做堆栈指针 当我们调用函数时 我们 分配我们需要的内存 仅仅通过递减堆栈 指针数值获得空间 当函数执行完毕之后 我们可以释放内存 只需要把堆栈指针增加至原来 的数值即可 调用这个函数之前 现在 如果你并不那么熟悉 堆栈或堆栈指针 我想让你从这个幻灯片中看到 堆栈分配是多么快 字面意思其实是 分配一个整数的消耗
那么 这就与堆形成了对比 堆更动态化 但比堆栈效率低 堆可以让你实现堆栈 所不能实现的功能 比如以动态周期分配内存 但那需要更高级的数据结构 那么 如果你要在堆上分配内存 你实际上要搜索堆数据结构 寻找闲置的 适当大小的内存块 用完之后要释放内存 你要重新把那个内存 插入适当的位置 很明显 这涉及的东西更多 相对于我们在堆栈中实现的 仅仅分配一个整数来说 但这些不见得是涉及 堆分配的必要的主消耗 因为同时可以给多线程分配内存 堆需要使用锁 来保护它的完整性 或其他同步机制 这是一个很大的消耗 如果你今天不注意 你的程序何时及在何处 在堆上分配内存 仅仅需要多考虑那么一点儿 你就能显著地改善性能 好了 让我们来看一些代码 看Swift都替我们做了什么 在这里 我们有一个点结构 有x和y存储属性 还有draw方法 我们用(0, 0)构造点 把point1赋值给point2 复制一下 并给point2.x赋一个为5的值 然后 我们要开始 使用point1和point2了 让我们来看一下 我们进入这个函数 在我们执行任何代码之前 我们已为point1和point2 实例在堆栈上分配了一个空间 因为点是一个结构 而x和y属性被存储在堆栈线中 那么 当我们用x为0 和y为0来构造点时 我们所要做的就是 初始化那块 内存 我们已经分配到堆栈上的 当把point1 赋值给point2时 我们仅仅是复制了那个点 并初始化了point2的内存 也是 我们已经分配到堆栈上的内存 请注意 point1和point2 是独立的实例 意思就是 当我们给point2.x 赋一个为5的值时 point2.x是5 但point1.x仍然是0 这就是值语义 然后我们继续 使用point1 使用point2 并完成函数的执行 那么 我们就可以来释放 point1和point2的内存 仅仅通过把堆栈指针的值增至 我们进入函数之前的值 跟同样的代码比较 这段代码仅使用了一个点 该点是一个类 而不是一个结构
好了 那么 我们进入这个函数 就跟刚才一样 我们给堆栈分配内存 但并不实际存储点的属性 我们要给point1和point2 分配内存引用 引用我们要分配到堆上的内存 那么 我们用(0, 0)构造点 Swift会锁住堆 并搜索数据结构 寻找适当大小的闲置内存块 然后 得到内存块后 我们要以x为0 进行初始化 y为0 并且我们要 把point1引用初始化 用内存地址 到那个堆上的内存 请注意 当我们在堆上分配时 Swift其实是为我们的点类 分配了四个字的存储 这跟当我们的点是结构时 所分配的两个字形成了对比 这是因为 现在的点是个类 除了为x和y存储的之外 我们又分配了两个字 Swift将替我们进行管理 那些字通过堆图中的 这些蓝色框来指示 当我们把point1 赋值给point2时 我们并不是要复制点的内容 不像当point1是一个结构时所做的那样 相反 我们要复制引用 point1和point2其实指的 正是堆上的同一个实例 意思是 当我们给point2.x 赋一个为5的值时 point1.x和 point2.x的值都为5 这就是引用的语义 可导致非计划的状态共享 然后 我们要使用point1 使用point2 然后Swift会替我们 释放这个内存 锁住堆 再分配闲置 内存块到适当的位置 然后我们就可以出栈了 好了 我们刚看到了什么? 我们看到类的构造 比结构的构造消耗更多 因为类需要堆式分配
由于类是在堆上分配的 并且有引用语义 所以类有一些强大的特性 如一致性和间接存储 但是 如果我们的抽象 不需要这些特性 我们最好还是用结构
而且结构不会导致像类那样的 非计划的状态共享 那么 我们该如何应用 以便改善某些Swift代码的性能呢 这儿有个例子 是我正在做的一个消息应用 那么 从根本上说 这来自视图层 我的用户们发送一条短信 在那条短信末端 我想画一个漂亮的气球 我的makeBalloon函数 是生成这个图片的函数 并支持不同的配置 或不同气球的整体配置空间 比如说 这个气球 我们看到是蓝色的 方位向右 有个尾部 我们还支持 比如说 灰色气球 方位向左 带气泡
makeBalloon函数执行要快 因为我会 频繁地调用它 在启动分配和用户滚动过程中 所以 我添加了这个缓存层 那么 对于任何给定的配置 我从不两次生成气球图片 如果我已生成了一次 我只需要从缓存中取出即可 我的实现方式是通过把颜色、 方位和尾部序列化 到一个键中 这个键是个字符串 这里有一些不妥当的地方 字符串不见得是这个键的 健壮类型 我用它来呈现这个配置空间 但我只是把我的狗的名字 放在了那个键中 所以 那儿不是很安全 而且字符串可以代表很多东西 因为它实际上把它的字符内容 间接地存储在堆上了 意思是我们每次 调用makeBalloon函数时 即使我们有缓存命中 我们也会引发堆的分配 看看是否可以做得更好 嗯 在Swift中 我们可以只用一个结构 来表示这个配置空间的 颜色、方位和尾部 这是一个比字符串更安全地 呈现配置空间的方式 因为结构在Swift中是头等类型 可以用作我们字典中的键 当调用makeBalloon函数时 如果我们有缓存命中 就不会有内存消耗 因为构造一个 像属性一这样的结构 不需要任何堆式分配 可以在堆栈上进行分配
更安全 也更快 让我们继续讲下一个 性能维度:引用计数
当我们谈堆式分配时 我隐瞒了一个详细信息 Swift如何了解何时释放
在堆上分配的内存是安全的呢? 嗯 答案是Swift会保持一个 引用个数的总计数 到堆上任何的实例中 并把它存储在实例本身 当你添加引用或移除引用时 就会增加或减少引用计数 当计数为零时 Swift就知道没有指向 堆上的这个实例的引用 而且释放那个内存很安全 引用计数的关键点是 这是个非常频繁的运算 实际上 比只增加和减少一个整数更复杂 首先 涉及到成对出现的间接层级 来执行增加和减少 但更重要的是 跟堆式分配一样 需要考虑线程的安全性 因为引用能被添加或移除到 任何堆实例 多线程上的 同时 我们实际上要自动增加 和减少引用计数 由于引用计数运算的频率高 会增加消耗
让我们返回去看点类和程序 看看Swift替我们做了什么
那么 在这里 我们有用来对比的一些伪代码 我们看到 我们的点获得了 一个附加属性refCount 并且Swift添加了一对调用 来保留– 或一个调用保留 和一个调用释放 保留会自动增加 我们的引用计数 释放会自动减少 我们的引用计数 这样 Swift就可以追踪 堆上的点上有多少激活的引用
好了 如果我们进行快速追踪 我们可以看到 在堆上构造点之后 那个点就被初始化为 引用计数为1 因为我们有一个 那个点的实时引用 我们查看整个程序 并把point1赋值给point2 我们现在就有两个引用了 那么Swift已经添加了一个调用 来自动增加点实例的引用计数 继续执行 一旦我们不再使用point1
Swift会添加一个调用 来自动减少引用计数 因为point1不再是 一个激活的引用了 它所关注的 同样地 一旦我们不再使用point2 Swift会添加另一个 自动减少引用计数 在这个点上 没有引用被 使用 我们的点实例 所以Swift就知道很安全 会锁住堆 并把那个内存块返回给它
如果是结构会怎么样呢? 结构是否涉及引用计数呢? 嗯 当我们构造点结构时 不会涉及任何堆式分配 当我们复制时 也不会涉及任何堆式分配 每个步骤都不会涉及引用 所以 点结构没有引用计数
那更复杂的结构呢? 在这里 我们有一个 包含文本的标签结构 类型为字符串类型 字体为UIFont 我们刚才提到过字符串 实际上是把它的 字符内容存储在堆上 所以需要引用计数 字体是一个类 也需要引用计数 我们看一下内存表现 标签有两个引用 当我们复制它时 我们实际上增加了两个引用 另一个文本存储 另一个字体 Swift的追踪方式是 这些堆式分配是通过 添加保留和释放的调用来实现的 那么在这里 我们看到 标签实际上会引发 两倍于那个类应有的引用计数
好了 总之 由于类是在堆上分配的 Swift得管理那个 堆式分配的使用期限 这是通过引用计数实现的
这并不容易 因为引用计数运算 相对频繁 另外引用计数具备原子性
这也是不愿使用结构的 另一个原因 但是 如果结构包含引用 也会进行引用计数 事实上 结构会 进行引用计数 相应地与它们所包含的 引用数量成比例地 所以 如果它们有一个 以上的引用 它们会保留一个类以上的 引用计数 让我们看看如何 把链应用到另一个示例
从我那个假设的消息应用中 那么 我的用户们 不愿意只发送文本消息 他们还想发送附件 如图片 那么 这个结构附件 在应用中是一个模型对象 有fileURL属性 即在磁盘上存储 这个附件的数据路径 有一个通用的唯一标识符 是一个唯一的、随机生成的标识符 这样 我们就可以 识别这个附件了 在客户端和服务器端 以及不同的客户设备上 还有一个mimeType 用于存储 这个附件所使用的数据类型 如JPG、PNG或GIF 很可能的情况是 唯一重要的代码 这个示例中 可能导致初始化失败 会检测 mimeType是否为 应用所支持的文件类型中的一个 因为我并不支持 所有mimeType 如果不支持 我们就会失败 反之 我们会初始化fileURL、 唯一标识和mimeType
那么 我们注意到有很多引用计数
并且 如果我们看一下 这个结构的内存表现 属性引发了引用计数 当传递属性时 由于在每个结构的底层 有进行堆式分配的引用
我们可以做得更好 首先 就像我们刚看到的 唯一标识是一个 意义明确的概念 它是一个随机生成的标识符 有128个位元 而且我们绝对不想让你随意 在唯一标识字段放入任何东西 且作为一个字符串 其实你是可放入任何东西的 嗯 今年 Foundation 增加了一个新的数值类型 为唯一标识 这很棒 因为它会 存储那128个位元 直接在结构线中 让我们用一下那个新数值类型 它要实现的是 移除所有引用计数所带来的消耗 为唯一标识字段 就是那个字符串 我们将获得更好的安全性 因为我们不能在这儿随意 放东西了 我们只能放唯一标识 棒极了 让我们看一下文件类型 以及如何实现文件类型检测 今天 我实际上只支持了 文件类型的一个闭集 JPG、PNG、GIF 我们都知道 Swift有强大的抽象体系 来表现固定集合 这是个枚举 我要把那个switch语句 放到可能导致 初始化失败的程序中 并把那些mimeType 映射到枚举中合适的案例中 那么 现在 我得到了 更多的mimeType枚举值 而且我还获得了更优化的性能 因为我不需要 存储 在堆上间接地 这些不同的案例 Swift实际上有一个 非常简洁、有效的方式 来写这段代码 就是使用 由原始字符串值支持的枚举 所以 这就是有效的代码 完全一样 除了更强大之外 有同样的性能特征 但写起来更便利 我们现在看一下附件结构 该方式使类型更安全 我们得到了类型非常强大的 唯一标识和mimeType字段 我们也不用做那么多引用计数 因为唯一标识和mimeType 不需要进行引用计数或堆式分配
好了 让我们继续看 最后一个性能维度 方法调度
在运行过程中 当调用一个方法时 Swift需要执行正确的实现 是否能在编译时确定 要执行的实现 这就是著名的静态调度 在运行过程中 我们只能直接跳到 正确的实现 这很酷 因为编译器实际上可以看到 要执行哪些实现 并且也可以 强行优化这个代码 包括一些像内联的东西 这跟动态调度形成了对比
动态调度 在编译时我们不能直接决定 要执行哪个实现 在运行过程中 我们实际上需要查找实现 然后跳到那儿 那么 对于实现本身 动态调度 比静态调度的消耗 并不多 只有一个间接层级 没有一个线程同步像我们之前 引用计数和堆式分配中的那样 但是这个动态调度阻塞了 编译器的可见性 所以 编译器可以 实现所有很酷的优化 为静态调度 动态调度 编译器不能推理通过
那么 我提到了内联 什么是内联? 嗯 让我们返回去看 我们熟悉的点结构 有一个x和y 还有一个draw方法 我还添加了这个 drawAPoint方法 drawAPoint方法在点中应用 只调用draw 很有意思 我的程序用(0,0)构造了一个点 并把那个点传给drawAPoint 嗯 drawAPoint函数 和point.draw方法 都是静态调度
意思就是编译器完全了解 要执行哪些实现 所以它实际上只要 把drawAPoint调度 替换为drawAPoint的实现 然后再把point.draw方法 因为它是个静态调度 替换为实际的 实现即可 point.draw的 那么 当我们在运行过程中 执行代码时 我们可以只构造点
然后运行实现 就完成了 我们不需要那两个静态调度 以及设置和销毁 相关联的堆栈调用 这很酷 这就回答了为什么是静态调度 以及静态调度 比动态调度要快多少的问题
然而 就像单一静态调度 与单一动态调度形成对比一样 并没有太多不同 但是一个完整的静态调度 编译器可以看到整个调度 因此 动态调度链 要在推理的每一步中被阻塞 在没有它的较高层级上 所以编译器要能 分解静态方法调度链 就像没有调用栈的 单一实现 这很酷 我们究竟为什么要这个 动态调度呢? 嗯 原因之一是它可以 启动很强大的东西 比如多态 我们看一个传统的 面向对象的程序 有一个可绘制的抽象超类 我可以定义一个 点子类和线子类 用自定义实现来覆盖draw 然后我有个程序 可以多态地 创建绘制的数组 可能包含线 可能包含点 可以分别调用draw 那么是如何实现的呢? 嗯 因为 可绘制的点和线都是类 我们可以创建一个数组 大小都一样 因为我们在数组中 通过引用来存储 然后 当我们查看每一个数组的时候 在数组上调用draw
我们明白或者希望 我们有一些直觉 为什么编译器 不能在编译时做出决定 哪个是要执行的正确的实现 因为这个d.draw可以是个点 可以是条线 这是不同的代码路径 那么 如何决定调用哪个呢? 嗯 编译器向类中添加了 另一个字段 是那个类的信息类型的指针 存储在静态内存中 因此 当我们调用draw时 编译器实际上替我们 生成的是一个对类型的查询 查找一个虚拟方法表 在类型和包含 指针的静态内存上 找到要执行的正确的实现 所以 如果我们修改了这个d.draw 编译器替我们做的是 我们看到 实际是查询虚拟方法表 找到要执行的正确的draw实现 然后把那个实际的实例 作为隐藏的自-参数传过来
好了 那么 我们看到了什么?
嗯 类默认动态地 调度它们的方法 这对于它本身并没有什么不同 但是如果形成方法链或其他形式 可以防止 内联优化 并且可以累计 但是 并不是所有类 都需要动态调度 如果你从未打算 给一个类创建子类 你可以把它标记为最终类 传给随后的同事 和未来的你 那是你的打算 编译器会注意到这一点 并动态地调度这些方法 此外 如果编译器 可以推理和证明 你从不打算在应用中 给类建立子类 它将适时地替你 把那些动态调度返回 到静态调度 如果你想了解更多实现方式 请参考去年关于 优化Swift性能的演讲
好了 我们讲到哪儿了?
在演讲的上半场 我想让你了解的是 问你自己的这些问题 无论何时 当你读和写Swift代码时 你都应该看和思考 “这个实例要在堆栈中 还是在堆中分配?” 当我传递这个实例时 我要引发多少引用计算? 当我在这个实例中调用方法时 是动态调度还是静态调度? 如果我们不需要执行动态调度 会影响我们的性能 如果你是Swift新手 或是使用代码库 从objective C 移植到Swift 你很可能会更好地利用结构 就像我们今天在例子中所看到的 为什么我使用结构而不是字符串
有一个问题是 “如何用结构写多态代码?” 我们还没有讲 嗯 答案是面向协议程序设计 现在让我们欢迎Arnold 来到台上给大家讲解
去吧 谢谢Kyle 大家好 我叫Arnold 让我们一起来看一下 协议类型和通用代码的实现 先讲协议类型 我们要看如何 存储和复制协议类型的变量 以及方法调度的运作
让我们再回到我们的应用中 这次 我们要用 协议类型来实现 这次我们不再用可绘制的抽象基类 我们要用声明了draw方法的 可绘制协议 并且我们有数值类型的点结构 和遵循协议的线结构
请注意 我们还有一个 遵循协议的SharedLine类 然而 我们决定不再让 由于非计划共享 而使类所具有的引用语义 出现 让我们停止它
我们程序仍然是多态的 我们可以 存储点类型和线类型的值 在可绘制的协议类型数组中 然而 跟以前相比 有一个不同点
请注意 我们的线数值类型结构 和点结构并不共享一个 共同的继承关系
做V-表调度所必须的 就是Kyle刚展示给我们的机制 那么 Swift是如何 调度正确的方法的呢? 在这个例子中 是通过彻底审查数组实现的
这个问题的答案是 一个基于表的机制 叫协议证明表 每种类型都有一张表 会在你的应用中实现协议 并且表中的条目 会链接到类型中的一个实现
好了 那么 现在我们了解 如何找到那个方法了 但是仍然有个问题 “如何把元素从数组中拿到表中?” 还有另一个问题
请注意 我们现在有 数值类型的线和点 线需要四个字
点需要两个字 它们的大小不一样 但数组需要 一致地存储元素 在数组中以固定的偏移量 那是如何实现的呢?
这个问题的答案是 Swift使用一个特殊存储布局 叫存在容器
里边有什么呢?
存在容器内的前三个字 是留给valueBuffer的
小类型 比如我们的点类型 只需要两个字 刚好能放进valueBuffer中 现在 你可能会说 “等一下 那线呢? 它需要四个字 我们该把它放哪儿去?” 嗯 在这种情况下 Swift会在堆上分配内存 并把值存入内存 而且会 给那个内存存一个指针
在存在容器中 现在 你看到了 点和线之间的不同点 因此 存在容器无论如何 得管理这个不同点 那么该如何实现呢?
嗯 答案是 还是 基于表的机制 在这个示例中 我们叫它值证明表
值证明表会管理值的有效期 在程序中 每种类型都有一张表
现在 让我们来看一下 局部变量的有效期 看这个表是如何运作的 那么 在协议类型的 局部变量的有效期的开始 Swift在那个表内部 调用了分配函数 在这个函数中 因为这个例子 有一个线值证明表 我们将在堆上分配内存并 给该内存存一个指针 存在容器的valueBuffer内
下一步 Swift要把复制 值从赋值源代码中 初始化局部变量的 到存在容器中 我们在这里有一个线 所以值证明表的复制条目 会做出正确的判断并把它 复制到valueBuffer中 在堆中分配的 好了 程序继续 我们现在是在局部变量 有效期的最后阶段 Swift会调用值证明 表的破坏条目 这将递减可能包含在类型中的 值的引用计数 线并没有任何引用计数 所以这里没什么需要注意的 然后 在最后 Swift会调用表中的 解除分配函数 再说一次 我们有一个线的值证明表 这将释放 在堆上为值分配的内存
好了 那么 我们已经看到了 Swift处理不同种类的值的一般性机制 但无论如何 它仍需要进入这些表 对吧?
嗯 答案很明显 值证明表的下一条 是一个引用 在存在容器中对值证明表的 一个引用
最后 如何进入协议证明表呢? 嗯 它是 再说一次 在存在容器中进行引用的
我们已经看到那个机制 关于Swift是如何管理 协议类型的值 让我们来看个例子 看看运行中的存在容器
在这个例子中 我们有一个函数 把协议类型参数当做局部参数 并在局部参数上执行draw方法 然后 我们的程序会创建一个 局部变量 可绘制的协议类型的 并用点对其进行初始化 然后把这个局部变量传给 一个drawACopy函数调用 作为它的参数
为了显示Swift编译器 为我们生成的代码 在这个例子中 我将使用Swift作为伪代码注释 那么 对于存在容器而言 我有一个结构 存储valueBuffer的三个字 还有一个值证明表 和协议证明表的引用
当drawACopy函数调用执行时 它会接收实参并把它传给函数 在生成的代码中我们看到 Swift把存在容器传给了 实参的函数
当函数开始执行时 函数为那个形参 创建了一个局部变量
并给它赋了一个实参
在所生成的代码中
Swift将在堆上分配 一个存在容器
下一步 它将 读取值证明表和协议证明表 从实参存在容器中 并在局部实参容器中 对字段进行初始化
下一步 它将调用值证明函数 分配缓冲区 如果必要的话 还会复制值
在这个例子中 我们传了一个点 所以就不需要 任何动态堆式分配了 这个函数只是从实参中把值复制 到局部存在容器的 valueBuffer中 然而 如果我们传一个线 这个函数将会分配缓冲区 并在缓冲区中复制值
下一步执行draw方法 Swift会从存在容器字段中 查询协议证明表 在那个表的固定偏移中 查询draw方法 并跳到那个实现 但是稍等一下
还有另一个值证明调用 就是projectBuffer 它为什么会在那儿?
嗯 draw方法把值的地址 当成了它的输入
请注意 这取决于值是否为 正好能放进内联缓冲区的小值 决定了这个地址是否为 存在容器的开始 或若我们有一个大值不适合放进 内联valueBuffer 那个地址就是在堆上分配的内存 的开始 那么 这个值证明函数 把这个不同点抽象化了 根据类型
然后执行draw方法 执行完毕 现在 我们是在函数的末端 意思就是 为形参创建的 局部变量超出了适用范围 所以Swift调用 一个值证明函数来破坏值 这将递减引用计数 如果值中有引用的话 并且如果分配了缓冲区 会释放缓冲区
函数执行完毕 移除了堆栈 也移除了在堆栈上创建的 局部存在容器
好了 这个工作量很大
是吧?
我想告诉你们的是 这项工作是使结合的值类型 如结构线和结构点还有协议 获得动态行为、动态多态性 我们可以 存储一条线和一个点 在可绘制的协议类型的数组中
如果你需要这个多态性 一切都值得你付出 跟使用类相比 就像Kyle在示例中 给我们演示的一样 因为类也要查询V-表 并且类还有附加的引用计数
好了 我们已经了解 如何复制局部变量 以及方法调度 如何处理协议类型值
让我们看一下存储属性
那么在这个例子中 我们有个对儿 包含两个存储属性 第一个和第二个属性
即可绘制的协议类型的
Swift是如何存储 这两个存储属性的呢? 嗯 是封闭结构的内联 那么 我们看一下- 当我们分配一个对儿时 Swift将存储这两个 非常必要的存在容器 对于在封闭结构内联中存储 那个对儿来说
然后 我们的程序就开始执行 并初始化 这个对儿 线和点 正如我们之前所看到的 对于线 我们将在堆上分配一个缓冲区 把点放到内联valueBuffer 并把它内联存储 到存在容器中
现在 这种呈现允许 在后面的程序中 存储一个不同类型的值 那么 程序继续执行 把线存入第二个元素 没什么问题 但我们现在有两个堆式分配了 好的 两个堆式分配 嗯 让我们用另外一个程序 来说明堆式分配的消耗
那么 再一次 我们创建一个线
然后创建一个对儿 并用线对这个对儿进行初始化 那么 我们有一个、 两个堆式分配 然后我们再一次复制那个对儿 堆栈中有两个存在容器 然后有两个堆式分配
现在 你可能会说 “Kyle刚告诉我们说 堆式分配的消耗很大” 四个堆式分配? 嗯 我们能做点什么吗?
嗯
请记住 存在容器能容纳三个字 可以把引用放进 那三个字中 因为引用基本上是一个字 那么 如果我们用类代替线来实现
并且类是个引用语义 因此它们 通过引用存储 该引用可放入 valueBuffer中
当我们把第一个引用复制 到对儿的第二个字段时 只复制了引用 我们消耗的只是 附加的引用计数增量
现在 你可能会说 “等一下 我们刚才不是听说 引用语义会引发 非计划的状态共享吗?” 那么 如果我们 存储到x1字段 通过对儿的第二个字段 第一个字段可以观察到变更 这并不是我们想要的结果 我们想要的是值语义 对吧? 嗯 我们能做点什么呢?
嗯 有一种技术叫复制并写入 可以帮助我们处理这个问题
那么 在我们写入类之前 我们要先查看它的引用计数 我们已经了解到 当同一实例有一个以上的 明显引用时 引用计数将大于一 二、三、四或五 如果是这种情况 在我们写入实例之前 我们先复制实例 然后写入那个副本 这将削弱状态
好了 让我们以线为例 看看是如何实现的
我们不直接在线的内部 实现存储 创建一个 叫LineStorage的类 这个类有线结构的所有字段 然后线结构引用这个存储
无论何时 当我们要读取值时 我们只需要 从那个存储内读取值 然而 当我们想要修改、 改变值时 我们首先要查看引用计数 是否大于一? 这是Uniquely Referenced调用要实现的 它只有一个功能 就是查看引用计数 是否大于或等于一?
如果引用计数比一大 大于一 就创建一个线存储的副本 并修改这个副本
好了 那么 我们已经了解 如何结合一个结构和一个类
使用复制和写入获得间接存储 让我们再回过来看例子 看看发生了什么
这次 我们使用间接存储
我们再创建一个线 这将在堆上创建一个线存储对象 然后 我们用那个线 对对儿进行初始化 这次 只复制线存储的引用
当我们复制线时
只会复制引用 引用计数会递增 这比堆式分配的消耗小多了 这是个不错的交易
好了 我们已经了解如何复制和存储 协议类型的变量 以及方法调度的运作方式 让我们看一下 这些对于性能的意义
如果协议类型包含能放进存在容器的 内联valueBuffer中 的小值 那么就不存在堆式分配
如果结构不包含引用 也就无所谓引用计数了 所以 这真的是一种快速编码 然而 由于间接 查询值证明表和协议证明表 我们得到了完整的动态调度 这就允许动态的多态行为
跟大值比较 大值会引发堆式分配 无论什么时候初始化 或赋予协议类型的变量
可能也将引发引用计数 如果大值结构包含引用的话
然而 我演示了一种技术 就是使用 可复制和写入的间接存储 承担堆式分配的部分消耗
这适用于消耗较少的引用计数
请注意 与使用类相比 很有优势 类还会引发引用计数
并在初始化时进行分配
这是笔好交易
好了 那么 我们回顾一下 简而言之 协议类型提供 多态的动态形式 值类型可以跟协议一起使用 并可以在协议类型的数组内 存储线和点 这是通过协议 实现的 和值证明表以及存在容器 复制大值会引发堆式分配 然而 我讲了一种 如何解决这个问题的技巧 即通过用间接存储 及复制和写入实现结构
好了 让我们再回到应用中 那么 在我们的应用中 我们需要draw一个函数 以协议类型为形参 然而 我们的使用方式是 我们要一直在具体类型上使用 在这里 我们要在线上使用
稍后 我们会在点上使用
然后我们会想“嗯
我们能在此使用通用代码吗?” 嗯 是的 我们能 让我们看一下 在演讲的最后 我要谈一下 通用类型的变量如何存储和复制 以及变量与方法调度的运作方式 那么 再返回我们的应用 这次 我们要使用通用代码来实现 DrawACopy方法把一个 泛型参数限制变得可绘制
程序的其它部分保持不变
那么 跟协议类型 有什么不同呢?
通用代码支持多态的 更静态的形式 也叫做参数多态性 每种调用情境都有一种类型 这是什么意思呢? 嗯 让我们看这个例子
我们有foo函数 这个函数把泛型参数T限制 变得可绘制 并把这个参数传给bar函数 这个函数再一次取走泛型参数T 然后 我们的程序创建一个点 并把这个点传给foo函数 当函数执行时
Swift会把泛型类型T绑定到 本次调用中所使用的类型 在这个例子中是点
当foo函数带着这个绑定执行时 它会进入bar的函数调用
局部变量的类型是刚发现的 也就是点 那么 泛型参数T再次 在这个调用情境中 通过点类型进行绑定 我们可以看到 在调用链中 类型随着参数被取而代之 这正是我们的用意...
通过更静态的多态形式 或参数化的多态性 那么 让我们看看Swift 是如何在后台实现的
让我们再次回到 drawACopy函数
在这个例子中 我们传递了一个点
就像我们使用协议类型时一样 有一个共享的实现
这个共享的实现 如果我可以给你展示它的代码 就像之前展示的协议类型的 代码一样 两者的代码看起来很相似
可以使用协议和值证明表 在函数内部执行运算
然而 因为每种调用情境 都有一个类型 所以 Swift不会在这里使用存在容器
相反 它可以传递值证明表 和协议证明表– 点的 在这次调用中使用的类型的 作为函数的附加参数
因此 在这个例子中 我们看到点和线的 值证明表被传过去了 在那个函数的执行过程中 当我们为参数创建局部变量时
Swift将使用值证明表 在堆上分配可能必要的缓冲区 并根据目的地 执行赋值源代码的副本 当在局部参数上执行 draw方法时 很相似 它将使用所传递的协议证明表 在表中查询 固定偏移的draw方法 并跳到那个实现
我刚刚说过了 这里没有存在容器 那么 Swift是如何 给局部参数 给为这个参数创建的 局部变量 分配所需要的内存呢?
嗯 它在堆栈上 分配一个valueBuffer 该valueBuffer仍是三个字 像点这样的小值 会放进valueBuffer
像线那样的大值 仍然会存在堆上 并且我们在局部存在内存里 存了一个指示器
所有这些都是为了使用值证明表
现在 你可能会问 “这样会更快吗?这样会更好吗?” “我可不可以不只使用协议类型?”
嗯 多态的静态形式 可以使编译器优化 叫做泛型特化 让我们具体看一下 这是我们的drawACopy函数 取走泛型参数 我们把一个点传给 那个函数调用方法
我们有静态多态 调用时有一种类型 Swift使用那个类型 在函数中替换泛型参数 并创建那个类型专用的 函数的一个版本 那么 在这里 我们现在有点函数 的drawACopy了 取走点类型的参数
函数内部的代码 仍然是那个类型专用的
就像Kyle给我们的展示的一样 这是非常迅捷的编码
Swift将给程序中的调用 所使用的每个类型都创建一个版本 那么 如果我们在点中 调用线上的drawACopy函数 它将特化并创建 那个函数的两个版本 现在 你可能会问“等一下 这可能会大量增加代码量 对吗?”
但是 由于静态类型信息 不能使编译器优化 Swift实际上可能会减少代码量 那么 比如说 它将内联点函数的drawACopy 然后进一步优化代码 因为现在有更多的情境了 因此 那个函数调用 基本上可以为这一条线 而正如Kyle所说的 这可以进一步减少为 draw的实现 现在 不再引用 点方法的drawACopy了 编译器也将把它移除 并在线示例中执行类似的优化 因此 几乎不太可能发生 编译器优化会增加代码量 有可能发生 但不一定是在这种情况下发生
好的 那么 我们已经了解特化 但还有一个问题 “何时进行?”
嗯 让我们看一个小例子 那么 我们定义了一个点 然后创建了那个点类型的局部变量- 把它初始化到一个点 然后把那个点传给 drawACopy函数 为了特化这个代码 Swift需要 在这次调用中推断出类型 它是可以实现的 因为它可以看到那个局部变量 再返回去看它的初始化 然后就会看到 它被初始化成了一个点
Swift还需要有 在特化过程中所使用的类型 的定义 和通用函数自身 这里的情况是这样的 在一个文件中进行了所有定义
这个文件能在很大程度上 提高整个模块优化 的优化几率 让我们看看这是为什么
比如说 我把点的定义挪到另一个文件中 如果我们分别编译那两个文件
当我编译UsePoint文件时 点的定义就不再可用了 因为编译器已经 分别编译了那两个文件 然而 对于整个模块优化 编译器将把两个文件 作为一个单元一起进行编译 洞悉点文件的定义 并进行优化 因为这能很大程度地 提高优化几率 现在 我们在Xcode 8中 启动了默认整体模块优化
好的 让我们 再返回来看我们的程序 那么 在我们的程序中 我们有可绘制的协议类型的对儿
并且 我们再次意识到 要如何使用它
无论什么时候我们要创建 一个对儿时 我们实际上是想 创建同一个类型的对儿 比如一对儿线或一对儿点
请记住 一对儿线的存储表示 会消耗两个堆式分配
当我们看这个程序时 我们注意到 我们可以在这里使用泛型类型
如果我们把对儿定义为泛型
然后那个泛型类型的第一个 和第二个属性有这种泛型类型 那么 编译器实际上可以强制 我们只能创建同一个类型的对儿 此外 我们也不能在后面的程序中 把点存储到一对儿线中
这的确是我们想要的结果
但是这样的表示对于性能来说 是好是坏呢? 让我们看一眼
那么 在此我们有对儿 这次 存储属性是泛型类型的 请记住我刚才说过的 不能在运行过程中改变类型
对于生成的代码来说 这意味着 Swift可以分配 闭合类型的存储内联
那么 当我们创建一对儿线时
给线分配的内存实际上 将被分配给内联的闭合对儿 不需要任何额外的堆式分配
这很酷
然而 就像我所说的 你不能再向不同类型的值中存储 那个存储属性了 但这正是我们想要实现的
好的 我们已经了解了 非专用代码如何使用 值证明表和协议证明表
以及编译器如何 通用函数的指定类型的版本 特化代码创建 让我们看一下它的性能
首先看包含结构的 特化通用代码 在这种情况下 我们有性能特性 与使用结构类型相同 因为正如我们所看到的 从本质上说 通用代码 看起来就好像是 你按照结构来写的这个函数 当我们复制结构类型的值时 不需要任何堆式分配 也不需要引用计数 如果结构不包含任何引用的话
而且我们有静态方法调度 进一步地优化编译器 并减少运行—执行时间
如果我们使用类类型 那么跟类类型相比 我们有跟类相似的特性 堆式分配、 创建实例和引用计数 是为了传递值 并对V-表进行动态调度
现在让我们看看 包含小值的非特化通用代码 不必给局部变量进行堆式分配 正如我们所看到的 因为小值可以放入分配到 堆栈中的valueBuffer
如果值不包含任何引用 也不会有引用计数 然而 我们在所有潜在的调用情境中 通过使用证明表 共享一个实现
好了 今天我们这场研讨会讲了 结构和类的性能特性 以及通用代码 和协议类型的运作方式 我们学到了什么?
哦 嗯 就这样吧 我忘了该讲哪个笑话了 如果我们使用大值和通用代码 就会引发堆式分配 但我之前展示过 那个技巧 也就是 使用间接存储方案 如果大值包含引用的话
然后还有引用计数 然后 我们再一次实现了动态调度 意思是 我们可以 共享一个通用实现 在代码中
好了 最后让我们看一下简述 总结 为应用中的实体 选择合适的抽象 动态运行时间要尽可能少
这将启动静态类型的检测 编译器可以确保 编译过程中程序的正确性 此外 编译器有更多的信息 来优化代码 从而得到更迅捷的代码 那么 如果你可以在程序中 使用值类型 如结构和枚举 表达实体 你将得到值语义 这很棒 不会出现非计划的状态共享 并且你将获得高度优化的代码
如果你因为需要 使用类 比如 一个实体或你正在使用 一个面向对象的框架 Kyle给我们展示了如何减少 引用计数的消耗的一些技巧
如果你的部分程序可以使用 一个更静态的多态形式来表达 你可以把通用代码 和值类型结合起来
并获得非常迅捷的代码 但共享那个代码的实现
如果你需要动态多态 比如 在我们的可绘制协议类型 示例的数组中 你可以把协议类型 和值类型相结合 得到跟使用类得到的代码 同样迅捷的代码
但你仍然可以停留在值语义内
如果你遇到了堆式分配问题 因为你在协议类型或泛型类型内复制值 我也给你们展示了一种技巧 也就是 使用可复制和写入的 简洁存储来处理
好的 那么 这里有一些与建模和性能 相关的演讲 我强烈推荐你们参加 今天下午的一场演讲 “UIKit应用中以协议和 值为导向的编程” 谢谢大家
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。