利用 Box 节省内存
预计阅读时间 5 分钟,发布时间为 2026 年 4 月 23 日。通过改变一些结构体的布局以及反序列化 JSON 文件的方式,为一个实际的 Rust 程序节省了 475 MB 内存,该程序原本占用 895 MB 内存。
目录
引言;实际用例;关于 Rust 结构体与内存;回收内存的改动;验证:证明影响;总结:简要回顾要点。
实际用例
程序将 中的所有 JSON 文件反序列化为 “Smithy Shape” 结构体。这些文件包含数千个类似示例的结构。程序使用了 [serde](https://docs.rs/serde/latest/serde/)。为清晰展示部分结构体,这是一堆包含结构体的结构体,有些是可选的,并且带有 serde 属性。按照这种方式反序列化,这些结构体在内存中占用了 895 MB。分析表明,大多数可选字符串缺失,可利用这一点减少内存占用,但需了解一些 Rust 的特性。
关于 Rust 结构体与内存
在 64 位平台上,一个字由 8 字节组成,存储一个 `usize` 需要这么多内存。一个 `String` 需要 3 个字,本身需要 24 字节,不包括堆上实际的字符串内容。编译器有特殊优化,`Option` 的大小和 `String` 相同。当 `SmithyServiceTrait` 结构体中的所有字符串都缺失时,它在内存中正好占用 120 字节。接着看结构体组合,如 `Container1` 结构体,其最小大小是 24 + 120 = 144 字节。若把 `Container` 结构体改成使用 `Option`,当 `some_string` 和 `trait` 都为 `None` 时,容器大小和 `Container1` 一样,使用 `Option` 未节省内存。将这一点应用到 `SmithyTraits` 上,可知标准实现会占用大量内存,这与 Java、Python、JavaScript 等语言中的类组合有根本区别。为让 Rust `Container` 在无内容存储时,可选内容只占用一个字的内存,要把内容放到堆上,如 `Container3`。前面提到的特殊优化也适用于 `Option>`。
回收内存的改动
改动包括检测结构体何时无用,让它们在父结构体中变为可选并移到堆上,实现自定义的反序列化器不存储空的无用结构体。如 `SmithyReference` 结构体的改动,同样 `SmithyShape` 也做了改动,将所有 `Option` 替换为 `Option>`,存储所有反序列化的 AWS 形状所需的内存减少了一半,节省了 475 MB。不过这种反序列化方式会消耗更多 CPU,但减少内存占用让整个任务更快完成。大量使用 `Box` 会导致堆碎片化,值得留意。
验证:证明影响
有经验后会对节省空间有直觉,但要严谨工作需检查改动是否有效并验证是否值得,所以需要进行测量。在 Rust 中,没有简单轻量的方法知道复合对象占用的总空间。解决方案是使用能提供自身状态信息的分配器(使用了 jemalloc),并比较反序列化前后的内存使用情况。在 `Cargo.toml` 中定义 “profile” 特性,在 `main.rs` 中声明使用这个分配器,接着在反序列化所有形状的函数中进行测量。[tikv_jemalloc_ctl](https://docs.rs/tikv-jemalloc-ctl/latest/tikv_jemalloc_ctl/stats/index.html) 提供了更多细节,在服务器应用中可能会很有用。
总结:简要回顾要点
每个 Rust 开发者需要理解并记住:复合结构体可能会消耗大量内存;通过检测字段内容是否无关紧要,将 `field: BigStruct` 设为可选是值得的;`field: Option` 即使在为 `None` 时,也至少会占用 `BigStruct` 的空间;可以使用 `field: Option>` 打破这种内存占用模式;使用 Serde 进行反序列化时,这些优化仍然可行。