类型大小
缩小经常实例化的类型可以提高性能。
例如,如果内存使用量很高,像 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::Z
在 match
模式中稍微不那么方便。
示例 1,
示例 2,
示例 3,
示例 4,
示例 5,
示例 6。
更小的整数
通常可以通过使用较小的整数类型来缩小类型。例如,虽然使用 usize
作为索引最自然,但通常可以将索引存储为 u32
、u16
或甚至 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
(通常是最常用的平台)很可能足以防止实际中的退化。