问题的答案。
Rust 采用了一种称为 利基值填充 的布局优化策略,消除了枚举判别符的存储开销,当变体包含具有无效位模式的类型时。编译器在类型的可表示范围内识别 "利基" 值——例如 NonZeroU32 的零值或对引用的空指针——并重新利用这些位模式来编码其他枚举变体,如 None。此转换依赖于有效负载类型具有内在属性或内部 rustc_layout 属性所定义的限制有效性范围。为了使一个类型能够作为有效的利基承载者,它必须显示出至少一种构造或读取时构成未定义行为的位模式,从而允许编译器将该模式保留用于枚举的替代变体,而无需分配额外的判别空间。
生活中的情况
在开发高频交易引擎时,我们的团队在存储数百万个订单时间戳的 Vec<Option<u64>> 时遇到了严重的缓存压力。每个可选时间戳由于对齐和判别开销消耗了16个字节,尽管时间戳本身严格为正的 Unix 纪元值。我们急需在不牺牲安全性或诉诸原始指针的情况下减少内存占用,而使用原始指针会使 Send 和 Sync 的跨线程处理保证变得复杂。
考虑的一个方法是使用原始 u64 值和带有不安全转换函数的哨兵零值手动位打包。这个解决方案承诺最大化内存效率,但引入了灾难性的风险:逻辑错误可能构造一个无效的 NonZeroU64 或解引用伪装成零的空指针,违反 Rust 的内存安全不变量。此外,它还会要求庞大的审计跟踪和 unsafe 块,这是团队希望避免的。
另外一个候选方案是直接使用 Optionstd::num::NonZeroU64,利用标准库保证的利基优化。这种方法保持了完全的类型安全和人性化的 match 表达式,同时确保 Option 的占用空间仅为8个字节而不是16个。主要的约束是我们必须保证时间戳永远不为零,这在我们的领域逻辑中是成立的,因为所有时间戳都在1970年之后。
我们选择了第二种解决方案,重构了我们的 Timestamp 新类型来包装 NonZeroU64 并在系统边界进行输入验证。结果是我们主要订单簿缓存的内存使用减少了50%。这种优化消除了缓存抖动,提高了30%的查找延迟,所有这些都在没有一行 unsafe 代码的情况下实现。
候选者通常遗漏的内容
为什么 Option<u32> 消耗8个字节而 Option<NonZeroU32> 仅消耗4个字节,以及该优化在嵌套类型如 Option<Option<NonZeroU32>> 中如何表现?
u32 类型允许所有 2^32 位模式作为有效,因此没有 "备用" 位模式可以被编译器重新利用作为 None 变体。因此,编译器必须附加一个判别字节(为了对齐而填充到4个字节),总共产生8个字节。相反,NonZeroU32 明确声明位模式 0x00000000 无效,创建了一个 Rust 用于编码 None 的 利基,使得结果 Option 的占用正好为4个字节。
对于嵌套结构,优化链有效:Option<Option<NonZeroU32>> 保持4个字节,因为外部 Option 利用了一种不同的无效位模式(例如,0x00000001)来自可用的 NonZeroU32 的利基空间。只要承载类型具有足够的无效位模式来容纳所有枚举判别值,这种递归优化就会继续。
像 #[repr(C)] 或 #[repr(u8)] 这样的显式布局属性如何与利基优化相互作用,以及这种相互作用为何在 FFI 边界中重要?
当应用 #[repr(C)] 或 #[repr(u8)] 时,程序员要求固定的内存布局,其中判别占据特定的偏移量和定义的大小。这个显式的表示有效地禁用了利基优化,确保与期望显式标签的 C 结构的 ABI 兼容,但迫使枚举消耗额外的空间用于判别。
在 FFI 上下文中,这一区别至关重要,因为 C 代码期望在可预测的、稳定的偏移量处找到判别。通过边界传递缺少显式 repr 属性的利基优化 Rust 枚举会导致未定义行为,而 #[repr(C)] 则在必要的内存效率成本下确保布局的稳定。
是什么阻止 MaybeUninit<T> 在枚举优化中充当利基承载者,即使 T 本身具有无效的位模式,例如在 Option<MaybeUninit<NonZeroU32>> 中?
MaybeUninit<T> 从架构上设计为能够保存任何位模式而不引发未定义行为,因为它的目的是表示可能未初始化的内存。因此,编译器将 MaybeUninit<T> 视为没有无效位模式,这意味着其有效性范围涵盖所有可能的 2^(8*sizeof(T)) 位组合。这个所有有效性排除了任何可供枚举优化的可用利基,不管 T 的属性如何。
因此,Option<MaybeUninit<NonZeroU32>> 占用8个字节——即 MaybeUninit<u32> 的大小加上判别填充——尽管底层的 NonZeroU32 拥有受限的有效性。该行为说明利基优化严格根据直接类型的有效性约束而非其潜在内容的传递属性进行操作。