类型大小

缩小经常实例化的类型可以提高性能。

例如,如果内存使用量很高,像 DHAT 这样的堆分析器可以识别热点分配点和涉及的类型。缩小这些类型可以减少峰值内存使用量,并可能通过减少内存流量和缓存压力来提高性能。

此外,大于 128 字节的 Rust 类型会使用 memcpy 而不是内联代码进行复制。如果在分析中大量出现 memcpy,DHAT 的 “copy profiling” 模式将告诉您热点 memcpy 调用的确切位置和涉及的类型。将这些类型缩小到 128 字节或更小可以通过避免 memcpy 调用和减少内存流量来使代码更快。

测量类型大小

std::mem::size_of 给出了类型的大小(以字节为单位),但通常您也想了解确切的布局。例如,由于单个超大变体,枚举可能会很大。

-Zprint-type-sizes 选项正是如此。它未在 rustc 的发布版本中启用,因此您需要使用 rustc 的夜间版本。以下是通过 Cargo 可能的一种调用:

RUSTFLAGS=-Zprint-type-sizes cargo +nightly build --release

以下是 rustc 的一个可能调用:

rustc +nightly -Zprint-type-sizes input.rs

它将打印出所有正在使用的类型的大小、布局和对齐方式的详细信息。例如,对于此类型:

#![allow(unused)]
fn main() {
enum E {
    A,
    B(i32),
    C(u64, u8, u64, u8),
    D(Vec<u32>),
}
}

它打印出以下内容,以及有关一些内置类型的信息。

print-type-size type: `E`: 32 bytes, alignment: 8 bytes
print-type-size     discriminant: 1 bytes
print-type-size     variant `D`: 31 bytes
print-type-size         padding: 7 bytes
print-type-size         field `.0`: 24 bytes, alignment: 8 bytes
print-type-size     variant `C`: 23 bytes
print-type-size         field `.1`: 1 bytes
print-type-size         field `.3`: 1 bytes
print-type-size         padding: 5 bytes
print-type-size         field `.0`: 8 bytes, alignment: 8 bytes
print-type-size         field `.2`: 8 bytes
print-type-size     variant `B`: 7 bytes
print-type-size         padding: 3 bytes
print-type-size         field `.0`: 4 bytes, alignment: 4 bytes
print-type-size     variant `A`: 0 bytes

输出显示以下内容。

  • 类型的大小和对齐。
  • 对于枚举,判别式的大小。
  • 对于枚举,每个变体的大小(从大到小排序)。
  • 所有字段的大小、对齐和顺序。(请注意,编译器已重新排序变体 C 的字段,以最小化 E 的大小。)
  • 所有填充的大小和位置。

或者,可以使用 top-type-sizes crate 以更紧凑的形式显示输出。

一旦您了解了热门类型的布局,就有多种方法可以缩小它们。

字段排序

Rust 编译器会自动对结构体和枚举的字段进行排序,以最小化它们的大小(除非指定了 #[repr(C)] 属性),因此您不必担心字段的顺序。但是,还有其他方法可以最小化热门类型的大小。

更小的枚举

如果一个枚举有一个超大的变体,请考虑将一个或多个字段放入 Box 中。例如,您可以将此类型更改为:

#![allow(unused)]
fn main() {
type LargeType = [u8; 100];
enum A {
    X,
    Y(i32),
    Z(i32, LargeType),
}
}

变为:

#![allow(unused)]
fn main() {
type LargeType = [u8; 100];
enum A {
    X,
    Y(i32),
    Z(Box<(i32, LargeType)>),
}
}

这会减小类型大小,但需要为 A::Z 变体进行额外的堆分配。如果 A::Z 变体相对较少,则更有可能实现净性能提升。Box 还会使得 A::Zmatch 模式中稍微不那么方便。 示例 1, 示例 2, 示例 3, 示例 4, 示例 5, 示例 6

更小的整数

通常可以通过使用较小的整数类型来缩小类型。例如,虽然使用 usize 作为索引最自然,但通常可以将索引存储为 u32u16 或甚至 u8,然后在使用点进行强制转换为 usize示例 1, 示例 2

Box 包装的切片

Rust 向量包含三个字:长度、容量和指针。如果您有一个向量在未来不太可能更改,您可以使用 Vec::into_boxed_slice 将其转换为boxed slice。Boxed slice 仅包含两个字,一个长度和一个指针。任何多余的元素容量都会被丢弃,这可能会导致重新分配。

#![allow(unused)]
fn main() {
use std::mem::{size_of, size_of_val};
let v: Vec<u32> = vec![1, 2, 3];
assert_eq!(size_of_val(&v), 3 * size_of::<usize>());

let bs: Box<[u32]> = v.into_boxed_slice();
assert_eq!(size_of_val(&bs), 2 * size_of::<usize>());
}

可以使用 slice::into_vec 将 boxed slice 转换回向量,而无需克隆或重新分配。

ThinVec

与 boxed slice 的替代方案是 thin_vec crate 中的 ThinVec。它在功能上等同于 Vec,但将长度和容量存储在元素相同的分配中(如果有的话)。这意味着 size_of::<ThinVec<T>> 仅为一个字。

ThinVec 在经常为空的向量类型中是一个不错的选择。如果枚举的最大变体包含一个 Vec,它也可以用于缩小。

避免退化

如果一个类型很热门,以至于它的大小会影响性能,最好使用静态断言来确保它不会意外退化。以下示例使用了 static_assertions crate 中的宏。

  // 这个类型经常使用。确保它不会意外增大。
  #[cfg(target_arch = "x86_64")]
  static_assertions::assert_eq_size!(HotType, [u8; 64]);

cfg 属性很重要,因为类型大小在不同的平台上可能会有所不同。将断言限制为 x86_64(通常是最常用的平台)很可能足以防止实际中的退化。