本篇是对Rust编程语言17_Rust的Vec优化[1]学习与记录
MiniVec
https://crates.io/crates/minivec
代码语言:javascript复制enum DataWithVec {
// tag,uint64,8字节
I32(i32), // 4字节,但需内存对齐到8字节?
F64(f64), // 8字节
Bytes(Vec<u8>), // 24字节
}
fn main() {
println!(
"DataWithVec这个Option类型占的内存空间为:{}字节",
std::mem::size_of::<DataWithVec>()
);
}
DataWithVec这个Option类型占的内存空间为:32字节
enum占用的栈内存大小=8 其中占内存最大的字段的内存
但当100个enum类型的数据中,有80%都是8字节数据,如f64,剩下的20%才是24字节的Vec,那占得比例:
代码语言:javascript复制
enum DataWithVec {
// tag,uint64,8字节
I32(i32), // 4字节,但需内存对齐到8字节?
F64(f64), // 8字节
Bytes(Vec<u8>), // 24字节
} // 32 byte
enum DataWithWithoutVec {
// tag,uint64,8字节
I32(i32), // 4字节,但需内存对齐到8字节?
F64(f64), // 8字节
} //16 byte
fn main() {
println!(
"DataWithVec这个Option类型占的内存空间为:{}字节",
std::mem::size_of::<DataWithVec>()
);
let ratio = (80 * std::mem::size_of::<DataWithWithoutVec>()) as f64
/ (100 * std::mem::size_of::<DataWithVec>()) as f64;
println!("ratio:{}", ratio)
}
代码语言:javascript复制DataWithVec这个Option类型占的内存空间为:32字节
ratio:0.4
利用率只有40%
剩下60%的都被浪费掉了
怎样可以缩减其大小?
最直接的想法是 用指针
代码语言:javascript复制pub enum DataWithBoxVec {
// tag,uint64,8字节
I32(i32), // 4字节,但需内存对齐到8字节?
F64(f64), // 8字节
Bytes(Box<Vec<u8>>), // 8字节
}// 16 byte
但这样会有性能问题
因为使用了二级指针(因为Vec里面也有一个指向data的指针),极有可能导致缓存命中率下降.需要再从内存中把数据取到缓存中
一次缓存缺失,会比缓存命中慢一个数量级
所以尽量不用二级指针
可以变成一级指针:
代码语言:javascript复制struct MiniVec<T> {
// len,capacity,T
data: * mut(usize,usize,T)
} //类似C语言的柔性数组
代码语言:javascript复制struct MiniVec {
// len,capacity,T
data: * mut(usize,usize,u8)
}
impl MiniVec {
pub fn new()-> MiniVec {
MiniVec {
data: // 8 8 一定数量的T
}
}
}
也可以用实现更具体更优的第三方库 minivec[2]
MiniVec大小就是8byte了
DataWithMiniVec就是16 byte了,比之前的32 byte减少了一倍
代码语言:javascript复制struct MiniVec<T> {
// len,capacity,T
data: * mut(usize,usize,T)
}
enum DataWithMiniVec {
I32(i32),
F64(f64),
Bytes(MiniVec<u8>),
}
smallvec
https://crates.io/crates/smallvec
new的时候不会分配内存
代码语言:javascript复制fn main() {
let vec: Vec<u8> = Vec::new();
assert_eq!(vec.capacity(), 0)
}
分配一次堆内存很昂贵,尽可能在栈上分配
当数量较少时,在栈上操作;元素数量较多时,才在堆上分配.比较有名的第三方库 smallVec
元素大小必须在编译期就确定,是个常数
有个阈值N.当元素数量小于N,则用栈内存.(上限 一般是几K到几M) 反之元素数量很多时,就要在堆上分配
Rust中的 MaybeUninit的作用及注意点
在 Rust 中,MaybeUninit<T>
是一个非常有用但需要谨慎使用的类型,它用于处理可能未初始化的内存。它是 Rust 标准库 std::mem
模块的一部分,提供了一种处理未初始化数据的安全方式。
MaybeUninit<T>
的主要用途是处理以下场景:
- 延迟初始化:当你有一个类型
T
,但你不想或无法立即初始化它时,可以使用MaybeUninit<T>
。这对于性能优化特别有用,尤其是在处理大型数组或复杂类型时。 - 避免不必要的初始化开销:对于某些类型,其默认初始化可能是昂贵的(例如,大型数组的零初始化)。使用
MaybeUninit<T>
可以避免这种开销。 - 与 FFI 交互:当与 C 语言接口进行交互时,你可能需要处理未初始化的内存或者由 C 代码初始化的内存。
MaybeUninit<T>
在这种情况下非常有用。
注意点
使用 MaybeUninit<T>
需要特别小心,因为不当的使用可能会导致未定义行为(UB),包括内存泄漏和数据损坏。以下是一些重要的注意事项:
- 安全性:访问
MaybeUninit<T>
的值之前必须确保它已被正确初始化。未初始化的内存访问是未定义行为。 - 初始化:你必须确保在使用
MaybeUninit<T>
的值之前,它已被完全且正确地初始化。 - Drop:
MaybeUninit<T>
本身不会自动调用其内部值的drop
方法。如果T
需要被适当地销毁,你需要手动调用drop
。 - 内存泄漏:如果你在
MaybeUninit<T>
中存储了需要手动管理的资源(例如,指向堆内存的指针),请确保适当地释放这些资源。
示例
下面是一个简单的示例,演示了 MaybeUninit<T>
的基本使用:
use std::mem::MaybeUninit;
fn main() {
// 创建一个未初始化的实例
let mut uninit_array: MaybeUninit<[u32; 5]> = MaybeUninit::uninit();
// 安全地初始化数据
let init_array = unsafe {
let init_array = uninit_array.as_mut_ptr();
for i in 0..5 {
// 初始化数组的每个元素
(*init_array)[i] = i as u32;
}
uninit_array.assume_init()
};
// 使用初始化后的数据
println!("{:?}", init_array);
}
在这个例子中,创建了一个可能未初始化的数组,并在确保安全的情况下初始化它。请注意,使用 unsafe
块是必须的,因为我们在操作原始指针,并且假设初始化是安全的。不过,确保这种安全是开发者的责任。不恰当的使用 unsafe
可能会导致严重的错误。
bitVec
https://crates.io/crates/bitvec
bitVec 一般是用来存储bool类型的
一个bit就可以标识是true还是false
代码语言:javascript复制struct BitVec {
bits: Vec<u64>
}
VecOption
https://crates.io/crates/vec-option
该优化可有可无
代码语言:javascript复制
struct VecOption<T> {
data: Vec<MaybeUninit<T>>,
flag:BitVec,
}
当为Some时,像flag push一个true
使用时,先访问flag.
比如访问索引为3的,先看看flag[3]是true还是false,根据其值得出是Some还是None
参考资料
[1]
Rust编程语言17_Rust的Vec优化: https://www.bilibili.com/video/BV1pv4y12725
[2]
minivec: https://crates.io/crates/minivec