类型大小

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

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

此外,Rust 中大于 128 字节的类型会使用 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

输出显示以下内容。

  • 类型的大小和排列。
  • 对于enums,判别子的大小。
  • 对于enums,每个变量的大小(从最大到最小排序)。
  • 所有字段的大小、对齐和排序。(请注意,编译器对变体C的字段进行了重新排序,以最小化E的大小。)
  • 所有padding的大小和位置。

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

一旦你知道了热型的布局,就有多种方法来收缩它。

字段顺序

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

更小的枚举

如果一个枚举有一个超大的变体,可以考虑将一个或多个字段装箱。例如,你可以改变这个类型。

#![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::Z的使用略微不符合人的直觉,特别是在匹配模式中。 Example 1, Example 2, Example 3, Example 4, Example 5, Example 6.

更小的intergers

通常可以通过使用较小的整数类型来缩小类型。例如,虽然对索引使用 “usize “是最自然的,但将索引存储为 “u32”、“u16”、甚至 “u8”,然后在使用点强制使用 “usize”,往往是合理的。 Example 1, Example 2.

Boxed Slices

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转换回一个矢量,而无需任何克隆或重新分配。

ThinVec

ThinVec是一个替代Boxed slices的选择,来自于thin_vec crate。它在功能上等同于Vec,但是在与元素相同的分配中存储长度和容量。这意味着size_of::<ThinVec<T>>只占用一个字。

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

Avoiding Regressions

如果一个类型足够热,它的大小会影响性能,那么最好使用静态断言来确保它不会意外地回归。下面的例子使用了static_assertions中的一个宏。

  // This type is used a lot. Make sure it doesn't unintentionally get bigger.
  #[cfg(target_arch = "x86_64")]
  static_assertions::assert_eq_size!(HotType, [u8; 64]);

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