如果在 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(步长) 是数组中到下一个元素的间距。由于对齐约束,元素之间可能产生空白空间(padding),所以 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 字节 padding(Swift ABI Type Layout)。
实践中,优先将 alignment 较大的属性放在一起,是减少 padding 的常见启发式方法。但它不能保证对所有类型都最优,最终结果必须用 MemoryLayout 测量。
当前 Swift 的 fragile struct 布局算法会按声明顺序放置存储属性。但字段顺序并不是语言或 ABI 的保证(Swift ABI · SE-0260)。需要精确 offset 的代码不能依赖观测值。
其次,只有实例数量很大时,这种优化才有意义。配置对象多出的 8 字节不同,一百万元素数组每个元素多 8 字节,总计就是 8 MB(Swift ABI Type Layout)。
一如既往,先测量。
enum 的魔法——Optional 标签免费的时候
enum 的布局充分体现了 Swift 编译器的聪明之处。它需要保存 case 区分值(标签),但会把标签隐藏在 payload 类型未使用的位模式(extra inhabitants)中。
典型案例是 Bool?。在当前 64 位 Apple 平台上,MemoryLayout<Bool?>.size 是 1。
Bool 在 1 个字节中只使用两个位模式(0、1),因此可以用剩余的一个模式表示 nil。
引用类型的 Optional 也是如此。Swift 类指针不会把 null 用作对象,因此用 0 表示 nil。在当前 64 位环境中,AnyObject? 的 size 是 8。
但并非所有 Optional 都免费。Int 使用 8 字节位模式表示值。因此在当前环境中,Int? 的 size 是 9,stride 是 16(Swift ABI)。
Optional 篇所说的“安全几乎是免费的”,对能够复用额外表示的类型而言是准确的。Optional 这一语法本身并不保证所有类型都零内存成本。
盒子的实测——单协议 existential 占 5 个 word
some·any 篇只说过“any 是一个有成本的盒子”,现在可以实际测量了。
protocol Shape {}
MemoryLayout<any Shape>.size // 40 — 当前 64 位环境
无类约束的单协议 any Shape 在当前 64 位环境中占 40 字节,即 5 个 word。组成是:3 个 word 的值缓冲区 + 类型元数据指针 + 一个 witness table 指针。
这里出现了重要分岔。如果所存值的 size 和 alignment 都满足 3-word inline buffer 的条件,就会直接放入缓冲区。
超过条件时,值会分配到单独的存储空间,缓冲区中保存指针。此时可能增加堆分配、引用计数和间接访问成本(Swift ABI · SE-0335)。
5 个 word 并不是所有 any 类型的固定大小。协议组合可能增加 witness table 指针。
类专用协议 existential 使用更小的布局,不需要值缓冲区和单独的元数据指针。
如果面向协议的代码意外变慢,可以考虑使用 some·泛型移除盒子,或缩小所存类型。实际效果必须通过 Instruments 和基准测试确认(SE-0335 Existential any)。
再看一下类实例的尺寸。在当前 64 位 Apple 平台上,类类型值的 stride 是 8,表示引用的大小,而不是对象的大小。
原生 Swift 堆对象通常有一个由元数据指针和内联引用计数信息组成的 header,在 64 位环境中为 16 字节。实际分配大小可能因属性对齐和分配器取整而更大(Swift HeapObject · Swift RefCount)。
ARC 篇提到的引用类型隐藏成本,就是这个 header 以及分配、释放和计数操作。若把数十万个小数据片段做成类,管理成本可能超过数据本身。
实践工具箱——重新测量、确认并质疑
下面整理处理这一层布局时的实践工具。
使用 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 较大的属性放在一起通常能减少 padding,但必须实测。
- enum 可以把标签隐藏在 payload 的 extra inhabitant 中。Bool? 和引用类型 Optional 不需要额外空间,但 Int? 等类型需要单独的标签空间。
- 64 位单一非类协议 existential 占 5 个 word。如果值的 size 或 alignment 超过 3-word 缓冲区条件,就可能产生单独存储空间和间接访问成本。
- 当前 64 位原生 Swift 对象的基本 header 通常为 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 inline buffer 与动态内存成本
- Swift HeapObject · Swift RefCount — Swift Runtime · 确认日期 2026-08-19 · 依据:原生堆对象 header 与内联引用计数

![[Swift进阶 #6] Swift内存布局:属性顺序会改变大小 封面图](/assets/images/posts/dfd50aa0-6496-4f00-a59f-21b04c18ed87/swift-memory-layout-size-stride-alignment.jpg)