类型大小
缩小经常实例化的类型可以提高性能。
例如,如果内存使用量很高,像 [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
”(通常是最广泛使用的平台)可能足以防止实际中的回落。