Swift编程Swift 开发者

Swift 的可选类型通过哪种内存布局优化,在包装引用类型时以没有额外存储的方式表示 `none` 情况?这种机制如何扩展到具有多个有效负载的枚举?

用 Hintsage AI 助手通过面试

问题的答案

Swift 通过一种称为 额外占用利用(或空闲位打包)的编译器优化来消除 Optionalnone 情况的存储开销。对于引用类型(闭包AnyObject),底层指针表示包含一个空地址(0x0),它不是有效的对象引用;Swift 重新利用这个空指针表示 Optional.none,而所有非空指针则表示 Optional.some。当将其扩展到具有多个有效负载的通用 枚举 时,编译器分析所有关联值类型的位模式,以识别共同的未使用值(空闲位)。如果所有有效负载类型至少共享足够的空闲位来编码案例计数,则枚举将把案例区分符存储在这些位中;否则,它会附加一个单独的标签字节或字。

生活中的情况

在为实时 3D 渲染引擎构建场景图时,团队需要为 200 万个场景节点存储可选的父引用。每个节点都是一个 实例,并且层次结构需要 Optional<Node> 来表示根节点(没有父节点)。

解决方案 A:并行布尔数组。
团队考虑维护一个单独的 ContiguousArray<Bool>ContiguousArray<Node> 一起使用,以指示父节点的存在。
优点: 明确的控制,语言无关的模式。
缺点: 由于访问两个不相交的内存区域,缓存局部性 被破坏;内存开销增加了 2MB(每个布尔值 1 字节,填充到对齐);重构树时的同步复杂性。

解决方案 B:哨兵节点模式。
使用一个全局单例“空节点”实例来表示缺失的父节点。
优点: 单个指针存储,没有可选开销。
缺点: 违反 类型安全;编译器无法防止意外操作哨兵;需要在整个代码库中进行防御性检查;如果哨兵持有对真实节点的引用,会引入引用循环。

解决方案 C:原生 Swift Optional。
在节点结构中直接采用 Optional<Node>
优点: 完全的编译时安全,符合 Swift 语法,零内存开销,因为 Optional 使用空指针表示 none
缺点: 需要理解此优化特定于引用类型;像 Int 这样的值类型会产生填充。

团队选择了 解决方案 C。由于 Node 是一个类,因此 Optional 包装器没有增加实例大小的字节。结果,与并行布尔方法相比,内存减少了约 16MB(消除了布尔存储和相关的对齐填充),同时获得了编译时保证,从而消除了在后续重构过程中整个类别的空指针解引用崩溃。

候选人常常忽视的内容

为什么 Optional<Int> 通常占用的内存比 Int 更多,而 Optional<AnyObject> 占用的空间与 AnyObject 相同?

Int 是一个 64 位的二进制补码整数,利用每个可能的位模式来表示其数值范围(-2^63 到 2^63-1),因此没有可用于 Optional 区分符的无效位模式(额外占用)。因此,编译器必须附加一个单独的字节(或字,根据对齐情况)来存储可选是 some 还是 none。相反,AnyObject(以及所有类引用)是指针,其中全零位模式(null)被保证为无效的对象地址;Optional 声称这个空表示其 none 情况,因此不需要额外的存储。

T 是一个类时,Optional<Optional<T>> 中对“缺失”的不同机器级表示有多少种,为什么这对相等性很重要?

存在两种不同的表示:外层的 .none(外层的空指针)和 .some(.none)(指向内部 null 的有效外层指针)。因为内部 Optional 已经消耗了空指针值来表示其自身的空性,外层 Optional 无法仅通过指针值来区分自身的 none 与一个包含内部 none.some。因此,外层需要一个单独的标签位,这两个概念上的“nil”状态是不相等的 (Optional(Optional.none) != Optional.none)。这种区分在从泛型 API 或 JSON 解码返回嵌套可选时至关重要,因为缺失的键产生外层 nil,而 null 值产生内部 nil。

当定义具有多个有效负载的枚举时,例如 case integer(Int), case boolean(Bool),编译器确定是否存储单独的标签字节还是将案例区分符嵌入到有效负载中,依据是什么?

编译器对关联值类型进行 空闲位分析Bool 只使用最低有效位,留下 7 位空闲。如果所有案例的有效负载提供足够的空闲位以唯一识别每个案例(例如,多个共享 null 额外占用的类引用),那么枚举可以将案例索引打包到这些未使用的位中。但是,IntBool 有不相交的空闲位模式(Int 没有),迫使编译器分配一个单独的标签字节(或字)来区分 integerboolean,使枚举的大小超出最大有效负载大小。