Swift 与 Objective-C

[Swift 深入解析 #6] Swift 内存布局:属性顺序会改变大小

struct 大小不是属性大小的简单总和。本篇整理 size、stride、alignment、填充、利用 extra inhabitants 的 Optional,以及 64 位 existential container 的实测。

10 分钟阅读
[Swift 深入解析 #6] Swift 内存布局:属性顺序会改变大小 封面图

在 Dispatch 篇了解“调用如何被翻译”后,这次来看“值如何放置”。

本文接续上一篇文章 Swift 深入解析 #5

struct 的大小是属性大小之和吗?不是。

只改变属性声明顺序就可能改变 struct 大小,意外吗?本篇讨论内存布局。

设计存放数十万个模型的数组,或 Instruments 中的内存图比预期更臃肿时,这些知识就很重要。

也会回答 some·any 篇留下的问题:“那个盒子实际有多大?”

三个尺寸 — size、stride、alignment

Swift 标准提供查询类型内存尺寸的工具:MemoryLayout

MemoryLayout<Int>.size       // 8
MemoryLayout<Bool>.size      // 1

struct Point { var x: Double; var y: Double }
MemoryLayout<Point>.size     // 16

尺寸有三个,各自含义不同。

size 是一个值的连续内存 footprint,不包含类型末尾用于对齐的 tail padding。

alignment 是类型可放置地址的规则。CPU 从 8 的倍数地址读取 8 字节值更高效,某些架构还要求这样。

因此每种类型都有“放在 n 的倍数地址”的限制。以下数据基于当前 64 位 Apple 平台:Double 的 alignment 为 8,Bool 为 1。

stride 是数组中到下一个元素的距离。对齐限制可能在元素间产生填充,因此 stride ≥ size。

这个经典示例展示了三者的关系。

struct Bad {
    var flag: Bool      // 1字节
    var value: Double   // 8字节 — 8的倍数地址
    var count: Int32    // 4字节
}
MemoryLayout<Bad>.stride   // 24

struct Good {
    var value: Double   // 8
    var count: Int32    // 4
    var flag: Bool      // 1
}
MemoryLayout<Good>.stride  // 16

内容相同,只调整顺序,数组就能节省 33%。Bad 需要在 flag 后为 value 对齐增加 7 字节填充(Swift ABI Type Layout)。

相同三个盒子只调整顺序,从 24 字节变为 16 字节的对齐对比图
内容相同,只改顺序就能从 24 变成 16

优先组合 alignment 较大的属性,是减少填充的常见经验法则。但不保证对所有类型最优,最终结果要用 MemoryLayout 测量。

当前 Swift 的 fragile struct 布局算法按声明顺序放置存储属性。但字段顺序不是语言或 ABI 的保证(Swift ABI · SE-0260)。需要精确偏移量的代码不能依赖观察结果。

这种优化只有实例数量很大时才有意义。单个配置对象多 8 字节不重要,但一百万元素数组每个多 8 字节就是 8 MB(Swift ABI Type Layout)。

一如既往,先测量。

enum 的魔法 — Optional 标签免费的时候

enum 布局体现了 Swift 编译器的巧思:需要保存 case 标签时,会将其隐藏在 payload 类型未使用的位模式(extra inhabitants)中。

典型案例是 Bool?。在当前 64 位 Apple 平台上,MemoryLayout<Bool?>.size 为 1。

Bool 在一个字节中只使用 0、1 两种模式,因此可以用剩余模式表示 nil。

引用类型 Optional 也一样。Swift 类指针不把 null 当作对象,因此用 0 表示 nil。当前 64 位下,AnyObject? 的 size 为 8。

但不是所有 Optional 都免费。Int 使用全部 8 字节位模式表示值。因此当前环境中 Int? 的 size 为 9、stride 为 16(Swift ABI)。

“安全几乎免费”只适用于可以复用额外表示的类型。Optional 语法本身不保证所有类型都零内存成本。

实测盒子 — 单一协议 existential 是 5 个 word

some·any 篇只说过“any 是有成本的盒子”,现在可以实际测量。

protocol Shape {}
MemoryLayout<any Shape>.size   // 40 — 当前 64位环境

当前 64 位环境中,单一无类约束协议 any Shape 占用 40 字节,即 5 个 word:3-word 值缓冲区、类型 metadata 指针和一个 witness table 指针。

关键在于值的 size 和 alignment 是否都满足 3-word 内联缓冲区的条件。

超过条件时,值会分配到独立存储空间,缓冲区保存指针。此时可能增加堆分配、引用计数和间接访问成本(Swift ABI · SE-0335)。

单一协议 existential 的 3-word 值缓冲区以及 metadata、witness table 指针剖面图
64 位单一非类协议 existential 为 5 个 word,超过缓冲区条件的值会使用独立存储空间

5 个 word 不是所有 any 类型的固定大小。协议组合可能增加 witness table 指针。

仅类协议 existential 使用更小的布局,不需要值缓冲区和单独的 metadata 指针。

如果面向协议的代码出乎意料地慢,可以考虑用 some·泛型移除盒子,或缩小承载类型。实际效果要通过 Instruments 和基准测试确认(SE-0335 Existential any)。

当前 64 位 Apple 平台上,类类型值的 stride 为 8,表示引用大小,而不是对象大小。

原生 Swift 堆对象通常有包含 metadata 指针和内联引用计数信息的头部;64 位下为 16 字节。实际分配大小可能因属性对齐和分配器取整而更大(Swift HeapObject · Swift RefCount)。

ARC 篇提到的引用类型隐藏成本,就是这个头部以及分配、释放、计数操作。创建数十万个小型类实例时,管理成本可能高于数据本身。

实用工具箱 — 重新测量、确认、保持怀疑

整理处理这一层的实用工具。

**用 MemoryLayout 验证假设。**设计大量生成的模型 struct 时,测量一次 stride 就足够形成习惯。

也可以在单元测试中加入类似 #expect(MemoryLayout<Tick>.stride <= 32) 的预算。以后新增属性超过预算时会立即暴露。

**用 Instruments Allocations 检查堆。**如果主要使用 struct 却有很多堆分配,嫌疑对象很明确。

any 盒子、闭包捕获、CoW(copy-on-write)存储,以及类实例。Allocations 的类型统计会告诉你是哪一项。

**copy-on-write 集合的错觉:**当前 64 位 Apple 平台上,MemoryLayout<[Int]>.stride 为 8。Array 值是间接引用堆存储的小型 struct,但不能把这个数值当作固定 ABI。

这就是不能因为包含数组的 struct stride 很小,就断定它“很轻”的原因。

真正的重量在堆存储中,由 Allocations 展示。CoW 原理已在另一篇文章中说明。

最后保持平衡:这篇知识并不是每天都会用到。

但了解“为什么”的价值会在问题出现时体现。内存图异常时,可以从嫌疑清单开始,而不是凭猜测,这就是深入一层的优势。

总结

  • 类型有三个尺寸:size 是不含 tail padding 的 footprint,alignment 是地址对齐规则,stride 是连续布局间距。大量生成的 struct 通常可通过组合高 alignment 属性减少填充,但必须实测。
  • enum 可以把标签隐藏在 extra inhabitants 中。Bool? 和引用类型 Optional 不需要额外空间,但 Int? 等类型需要独立的标签空间。
  • 64 位单一非类协议 existential 为 5 个 word。值的 size 或 alignment 超过 3-word 缓冲区条件时,可能产生独立存储和间接成本。
  • 当前 64 位原生 Swift 对象的基本头部通常为 16 字节。实际分配大小可能因属性和分配器对齐而更大。
  • 使用 MemoryLayout 验证设计,使用 Instruments Allocations 实测。没有测量的布局优化属于过早优化。

下一篇讨论编译时魔法:Swift 宏,即在 #Preview 和 @Observable 背后让代码编写代码的机制。

来源与确认标准

  • MemoryLayout — Apple Developer Documentation · 确认 2026-08-19 · 依据:size、alignment、stride 的 API 定义
  • Swift ABI Type Layout — Swift 项目 · 官方文档 · 确认 2026-08-19 · 依据:struct、enum、existential container 布局
  • SE-0260 Library Evolution — Swift Evolution · 确认 2026-08-19 · 依据:struct 字段顺序的语言与 ABI 保证范围
  • SE-0335 Existential any — Swift Evolution · 确认 2026-08-19 · 依据:existential 的 3-word 内联缓冲区与动态内存成本
  • Swift HeapObject · Swift RefCount — Swift Runtime · 确认 2026-08-19 · 依据:原生堆对象头部与内联引用计数

延伸阅读

Swift 深入解析系列