为啥 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_capacitynew 慢。仅对于较大的最终向量长度(例如 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 对于小的最终长度慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥向量长度 SIMD 代码比普通 C 慢

为啥 Spark 比 Hadoop Map Reduce 快

为啥v-if和v-for不建议同时使用

为啥字符串-字符串连接比字符串长连接更快? [关闭]

为啥读取整个 hdf5 数据集比读取切片更快

为啥电机FOC控制里面反clarke变换 Valpha 和 Vbeta 的位置变了?