为啥 Vec::with_capacity 比 Vec::new 对于小的最终长度慢?
Posted
技术标签:
【中文标题】为啥 Vec::with_capacity 比 Vec::new 对于小的最终长度慢?【英文标题】:Why is Vec::with_capacity slower than Vec::new for small final lengths?为什么 Vec::with_capacity 比 Vec::new 对于小的最终长度慢? 【发布时间】:2022-01-19 16:16:01 【问题描述】:考虑一下这段代码。
type Int = i32;
const MAX_NUMBER: Int = 1_000_000;
fn main()
let result1 = with_new();
let result2 = with_capacity();
assert_eq!(result1, result2)
fn with_new() -> Vec<Int>
let mut result = Vec::new();
for i in 0..MAX_NUMBER
result.push(i);
result
fn with_capacity() -> Vec<Int>
let mut result = Vec::with_capacity(MAX_NUMBER as usize);
for i in 0..MAX_NUMBER
result.push(i);
result
两个函数产生相同的输出。一个使用Vec::new
,另一个使用Vec::with_capacity
。对于较小的 MAX_NUMBER
值(如示例中所示),with_capacity
比 new
慢。仅对于较大的最终向量长度(例如 1 亿),使用 with_capacity
的版本与使用 new
的版本一样快。
100 万个元素的火焰图
一亿个元素的火焰图
我的理解是,如果最终长度已知,with_capacity
应该总是更快,因为堆上的数据被分配一次,应该导致单个块。相比之下,带有new
的版本将向量MAX_NUMBER
增长了倍,这会导致更多的分配。
我错过了什么?
编辑
第一部分是使用debug
配置文件编译的。如果我在Cargo.toml
中使用具有以下设置的release
配置文件
[package]
name = "vec_test"
version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = 3
debug = 2
对于1000万的长度,我仍然得到以下结果。
【问题讨论】:
Vec
的容量呈指数增长,我相信,所以对于相对较小的值,预计它不会分配 太多,尽管它的速度较慢很奇怪, 分配一次应该总是更快。
为什么Vec
(最终是一个列表并表示为(pointer, capacity: usize, len: usize)
)会呈指数级增长?
只要Vec
满了,它的容量就会翻倍,从而产生指数级增长。指数增长允许追加是 O(1) 摊销 - 不是 每个 追加都是恒定时间,但平均而言,它们是因为随着向量的增长,容量翻倍越来越少。不过,我不确定你的意思是什么,Filipe?
Rust 的增长因子是 2X。您可以在RawVec::grow_amortized
中看到它,这是Vec
内部使用的。
调整您的发布配置文件,使其保留所有符号。不使用优化构建会破坏对这些原语进行基准测试的目的。
【参考方案1】:
我无法在综合基准测试中重现这一点:
use criterion::criterion_group, criterion_main, BenchmarkId, Criterion;
fn with_new(size: i32) -> Vec<i32>
let mut result = Vec::new();
for i in 0..size
result.push(i);
result
fn with_capacity(size: i32) -> Vec<i32>
let mut result = Vec::with_capacity(size as usize);
for i in 0..size
result.push(i);
result
pub fn with_new_benchmark(c: &mut Criterion)
let mut group = c.benchmark_group("with_new");
for size in [100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000].iter()
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size|
b.iter(|| with_new(size));
);
group.finish();
pub fn with_capacity_benchmark(c: &mut Criterion)
let mut group = c.benchmark_group("with_capacity");
for size in [100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000].iter()
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size|
b.iter(|| with_capacity(size));
);
group.finish();
criterion_group!(benches, with_new_benchmark, with_capacity_benchmark);
criterion_main!(benches);
这是输出(已删除异常值和其他基准测试内容):
with_new/100 time: [331.17 ns 331.38 ns 331.61 ns]
with_new/1000 time: [1.1719 us 1.1731 us 1.1745 us]
with_new/10000 time: [8.6784 us 8.6840 us 8.6899 us]
with_new/100000 time: [77.524 us 77.596 us 77.680 us]
with_new/1000000 time: [1.6966 ms 1.6978 ms 1.6990 ms]
with_new/10000000 time: [22.063 ms 22.084 ms 22.105 ms]
with_capacity/100 time: [76.796 ns 76.859 ns 76.926 ns]
with_capacity/1000 time: [497.90 ns 498.14 ns 498.39 ns]
with_capacity/10000 time: [5.0058 us 5.0084 us 5.0112 us]
with_capacity/100000 time: [50.358 us 50.414 us 50.470 us]
with_capacity/1000000 time: [1.0861 ms 1.0868 ms 1.0876 ms]
with_capacity/10000000 time: [10.644 ms 10.656 ms 10.668 ms]
with_capacity
的运行速度始终比with_new
快。最接近的是在 10,000 到 1,000,000 个元素的运行中,with_capacity
仍然只花费了大约 60% 的时间,而其他人则花费了一半或更少的时间。
我突然想到可能会发生一些奇怪的 const-propagation 行为,但即使使用硬编码大小的单个函数(playground 为简洁起见),该行为也没有显着变化:
with_new/100 time: [313.87 ns 314.22 ns 314.56 ns]
with_new/1000 time: [1.1498 us 1.1505 us 1.1511 us]
with_new/10000 time: [7.9062 us 7.9095 us 7.9130 us]
with_new/100000 time: [77.925 us 77.990 us 78.057 us]
with_new/1000000 time: [1.5675 ms 1.5683 ms 1.5691 ms]
with_new/10000000 time: [20.956 ms 20.990 ms 21.023 ms]
with_capacity/100 time: [76.943 ns 76.999 ns 77.064 ns]
with_capacity/1000 time: [535.00 ns 536.22 ns 537.21 ns]
with_capacity/10000 time: [5.1122 us 5.1150 us 5.1181 us]
with_capacity/100000 time: [50.064 us 50.092 us 50.122 us]
with_capacity/1000000 time: [1.0768 ms 1.0776 ms 1.0784 ms]
with_capacity/10000000 time: [10.600 ms 10.613 ms 10.628 ms]
您的测试代码只调用每个策略一次,因此可以想象在调用第一个策略后您的测试环境发生了变化(潜在的罪魁祸首是 cmets 中的@trent_also_cl 建议的堆碎片,尽管可能还有其他原因:cpu boosting /节流、空间和/或时间缓存位置、操作系统行为等)。像criterion
这样的基准测试框架通过多次迭代每个测试(包括预热迭代)来帮助避免很多此类问题。
【讨论】:
以上是关于为啥 Vec::with_capacity 比 Vec::new 对于小的最终长度慢?的主要内容,如果未能解决你的问题,请参考以下文章