Vector 为什么通过替换默认值来填充Vec比使用预设容量填充要快得多?

Vector 为什么通过替换默认值来填充Vec比使用预设容量填充要快得多?,vector,rust,Vector,Rust,前言:我通常不是一个优化器 大多数情况下,在解决Rust中的编码难题时,我使用Vec::with_capacity初始化向量,然后通过将项目推到向量上插入。在大多数情况下,这很好,但我最近遇到了一个难题,需要一个更快的程序,这启发了我重新思考我的方法 因为我知道向量的容量正好是某个数字,所以我决定将我通常的方法与_capacity和push方法的结果进行比较,以创建一个充满0的向量并替换它们。这是我用来对两个操作进行基准测试的代码: #![feature(test)] extern crate

前言:我通常不是一个优化器

大多数情况下,在解决Rust中的编码难题时,我使用
Vec::with_capacity
初始化向量,然后通过
将项目推到向量上插入。在大多数情况下,这很好,但我最近遇到了一个难题,需要一个更快的程序,这启发了我重新思考我的方法

因为我知道向量的容量正好是某个数字,所以我决定将我通常的
方法与_capacity
push
方法的结果进行比较,以创建一个充满0的向量并替换它们。这是我用来对两个操作进行基准测试的代码:

#![feature(test)]

extern crate test;

#[cfg(test)]
mod tests {
    use test::Bencher;

    // Create a vector with a capacity of 10,000 u16s
    // and populate it by pushing.
    #[bench]
    fn push_fill(b: &mut Bencher) {
        b.iter(|| {
            let mut v: Vec<u16> = Vec::with_capacity(10000);
            for i in 0..10000 as u16 {
                v.push(i);
            }
        })
    }

    // Create a vector of 10,000 u16s, initialize them
    // to 0, and then replace them to populate the vector.
    #[bench]
    fn replace_fill(b: &mut Bencher) {
        b.iter(|| {
            let mut v: Vec<u16> = vec![0u16; 10000];
            for i in 0..10000 {
                v[i] = i as u16;
            }
        })
    }
}
我对时间上的差异感到惊讶,特别是考虑到我预计
replace
版本需要更长的时间(考虑到它必须创建一个充满填充符的向量,然后用实际数据替换填充符数据)


为什么
replace\u fill
push\u fill
快得多,有没有直观的原因?这两个功能之间的区别是什么?

如果有疑问,请检查组件

你可以使用或在操场;虽然我更喜欢godbolt,因为它使用高亮显示将程序集部分与源代码相匹配,从而更易于探索


在上面的链接中,
replace\u fill
功能优化为:

example::replace_fill:
  push rbp
  mov rbp, rsp
  sub rsp, 48
  lea rdx, [rbp - 24]
  mov edi, 20000
  mov esi, 2
  call __rust_alloc_zeroed@PLT
  test rax, rax
  je .LBB3_4
  movdqa xmm0, xmmword ptr [rip + .LCPI3_0]
  mov ecx, 32
  movdqa xmm1, xmmword ptr [rip + .LCPI3_1]
  movdqa xmm2, xmmword ptr [rip + .LCPI3_2]
  movdqa xmm3, xmmword ptr [rip + .LCPI3_3]
  movdqa xmm4, xmmword ptr [rip + .LCPI3_4]
  movdqa xmm5, xmmword ptr [rip + .LCPI3_5]
.LBB3_2:
  movdqu xmmword ptr [rax + 2*rcx - 64], xmm0
  movdqa xmm6, xmm0
  paddw xmm6, xmm1
  movdqu xmmword ptr [rax + 2*rcx - 48], xmm6
  movdqa xmm6, xmm0
  paddw xmm6, xmm2
  movdqu xmmword ptr [rax + 2*rcx - 32], xmm6
  movdqa xmm6, xmm0
  paddw xmm6, xmm3
  movdqu xmmword ptr [rax + 2*rcx - 16], xmm6
  movdqa xmm6, xmm0
  paddw xmm6, xmm4
  movdqu xmmword ptr [rax + 2*rcx], xmm6
  paddw xmm0, xmm5
  add rcx, 40
  cmp rcx, 10032
  jne .LBB3_2
  mov esi, 20000
  mov edx, 2
  mov rdi, rax
  call __rust_dealloc@PLT
  add rsp, 48
  pop rbp
  ret
.LBB3_4:
  mov rax, qword ptr [rbp - 24]
  movups xmm0, xmmword ptr [rbp - 16]
  movaps xmmword ptr [rbp - 48], xmm0
  mov qword ptr [rbp - 24], rax
  movaps xmm0, xmmword ptr [rbp - 48]
  movups xmmword ptr [rbp - 16], xmm0
  lea rdi, [rbp - 24]
  call __rust_oom@PLT
  ud2
后一部分(LBB3_4)是OOM处理,因此从未使用过。因此,执行流程如下:

  • 示例::替换填充
    ,它执行分配+初始设置
  • .LBB3_2
    哪个是循环
有两个值得注意的要素:

  • 那里根本没有出现
    Vec
    代码
  • 这些是矢量指令

另一方面,
push_fill
有点复杂:

example::push_fill:
  push rbp
  mov rbp, rsp
  push r15
  push r14
  push rbx
  sub rsp, 40
  lea rdx, [rbp - 48]
  mov edi, 20000
  mov esi, 2
  call __rust_alloc@PLT
  mov rcx, rax
  test rcx, rcx
  je .LBB2_11
  mov qword ptr [rbp - 48], rcx
  mov qword ptr [rbp - 40], 10000
  mov qword ptr [rbp - 32], 0
  xor r15d, r15d
  lea r14, [rbp - 48]
  xor esi, esi
.LBB2_2:
  mov ebx, r15d
  add bx, 1
  cmovb bx, r15w
  jb .LBB2_3
  cmp rsi, qword ptr [rbp - 40]
  jne .LBB2_9
  mov rdi, r14
  call <alloc::raw_vec::RawVec<T, A>>::double
  mov rcx, qword ptr [rbp - 48]
  mov rsi, qword ptr [rbp - 32]
.LBB2_9:
  mov word ptr [rcx + 2*rsi], r15w
  mov rsi, qword ptr [rbp - 32]
  inc rsi
  mov qword ptr [rbp - 32], rsi
  movzx eax, bx
  cmp eax, 10000
  mov r15w, bx
  jb .LBB2_2
.LBB2_3:
  mov rsi, qword ptr [rbp - 40]
  test rsi, rsi
  je .LBB2_5
  add rsi, rsi
  mov rdi, qword ptr [rbp - 48]
  mov edx, 2
  call __rust_dealloc@PLT
.LBB2_5:
  add rsp, 40
  pop rbx
  pop r14
  pop r15
  pop rbp
  ret
.LBB2_11:
  movups xmm0, xmmword ptr [rbp - 40]
  movaps xmmword ptr [rbp - 64], xmm0
  movaps xmm0, xmmword ptr [rbp - 64]
  movups xmmword ptr [rbp - 40], xmm0
  lea rdi, [rbp - 48]
  call __rust_oom@PLT
  ud2
  mov rbx, rax
  lea rdi, [rbp - 48]
  call core::ptr::drop_in_place
  mov rdi, rbx
  call _Unwind_Resume@PLT
  ud2
此方法来自于实现。当与信任长度迭代器(如本例)一起使用时,它将在必要时执行单个“增长”步骤,然后在不再次检查的情况下进行推送

总成不像
更换填充
那样精巧,但看起来仍然很不错:

example::extend_fill:
  push rbp
  mov rbp, rsp
  sub rsp, 64
  mov qword ptr [rbp - 24], 2
  xorps xmm0, xmm0
  movups xmmword ptr [rbp - 16], xmm0
  lea rdx, [rbp - 48]
  mov edi, 20000
  mov esi, 2
  call __rust_alloc@PLT
  test rax, rax
  je .LBB4_7
  mov qword ptr [rbp - 24], rax
  mov qword ptr [rbp - 16], 10000
  xor ecx, ecx
  movdqa xmm0, xmmword ptr [rip + .LCPI4_0]
  movdqa xmm1, xmmword ptr [rip + .LCPI4_1]
  jmp .LBB4_2
.LBB4_6:
  movd xmm2, edx
  pshuflw xmm2, xmm2, 0
  pshufd xmm2, xmm2, 80
  movdqa xmm3, xmm2
  paddw xmm3, xmm0
  paddw xmm2, xmm1
  movdqu xmmword ptr [rax + 2*rcx + 32], xmm3
  movdqu xmmword ptr [rax + 2*rcx + 48], xmm2
  add rdx, 16
  mov rcx, rdx
.LBB4_2:
  movd xmm2, ecx
  pshuflw xmm2, xmm2, 0
  pshufd xmm2, xmm2, 80
  movdqa xmm3, xmm2
  paddw xmm3, xmm0
  paddw xmm2, xmm1
  movdqu xmmword ptr [rax + 2*rcx], xmm3
  movdqu xmmword ptr [rax + 2*rcx + 16], xmm2
  lea rdx, [rcx + 16]
  cmp rdx, 10000
  jne .LBB4_6
  mov qword ptr [rbp - 8], 10000
  mov rsi, qword ptr [rbp - 16]
  test rsi, rsi
  je .LBB4_5
  add rsi, rsi
  mov rdi, qword ptr [rbp - 24]
  mov edx, 2
  call __rust_dealloc@PLT
.LBB4_5:
  add rsp, 64
  pop rbp
  ret
.LBB4_7:
  movups xmm0, xmmword ptr [rbp - 40]
  movaps xmmword ptr [rbp - 64], xmm0
  movaps xmm0, xmmword ptr [rbp - 64]
  movups xmmword ptr [rbp - 40], xmm0
  lea rdi, [rbp - 48]
  call __rust_oom@PLT
  ud2

我鼓励您尝试一下,并且通常要熟悉Rust迭代器:甜美的代码,良好的性能,它们是您需要的工具。

我不希望它有多大区别,但您的两个循环并不相同。一个迭代
usize
s,一个迭代
i16
s切换到
0..10000作为u16
v[i作为usize]
中的
replace\u-fill
几乎没有影响-
push\u-fill
给出
30213 ns/iter(+/-12046)
replace\u-fill
给出
1982 ns/iter(+/-2291)
Vec.push()
具有增加
Vec
len
字段的逻辑,并检查是否需要增加容量。
replace\u fill()
中的范围检查可能会被优化掉,由于缓存的存在,在内存中的第二次检查可能是微不足道的。创建10000次
0u16
的向量只需
malloc
+
memset
或者在这种情况下甚至可能只有一次
calloc(10000,sizeof(uint16\t))
–没什么费时的。嗯,@KBiermann,听起来不错。如果你把它作为一个答案发布,我会接受它。我会说
让mut v:Vec=(0..10000).collect()更惯用。@Hauleth:我不想对用例作太多假设。对我来说,反复使用相同的缓冲区是一种非常常见的情况,在这种情况下,
clear
+
extend
是必要的
collect
可能有效,
extend
刚好有效。“如果有疑问,请检查程序集!”,不是每个人都想了解程序集,这就是我们为什么使用rust编写代码的原因;)@星门:这有点像在开玩笑;但这也是唯一真正的答案。归根结底,检查程序集比预测优化器可以(或没有)用原始代码做什么要简单得多。如何通过操场检查程序集?我对那件事没有深入研究太多。此外,虽然我可以看到大会显然更加复杂,但我不明白为什么,除非我读过K.比尔曼在原始帖子上的评论。
example::extend_fill:
  push rbp
  mov rbp, rsp
  sub rsp, 64
  mov qword ptr [rbp - 24], 2
  xorps xmm0, xmm0
  movups xmmword ptr [rbp - 16], xmm0
  lea rdx, [rbp - 48]
  mov edi, 20000
  mov esi, 2
  call __rust_alloc@PLT
  test rax, rax
  je .LBB4_7
  mov qword ptr [rbp - 24], rax
  mov qword ptr [rbp - 16], 10000
  xor ecx, ecx
  movdqa xmm0, xmmword ptr [rip + .LCPI4_0]
  movdqa xmm1, xmmword ptr [rip + .LCPI4_1]
  jmp .LBB4_2
.LBB4_6:
  movd xmm2, edx
  pshuflw xmm2, xmm2, 0
  pshufd xmm2, xmm2, 80
  movdqa xmm3, xmm2
  paddw xmm3, xmm0
  paddw xmm2, xmm1
  movdqu xmmword ptr [rax + 2*rcx + 32], xmm3
  movdqu xmmword ptr [rax + 2*rcx + 48], xmm2
  add rdx, 16
  mov rcx, rdx
.LBB4_2:
  movd xmm2, ecx
  pshuflw xmm2, xmm2, 0
  pshufd xmm2, xmm2, 80
  movdqa xmm3, xmm2
  paddw xmm3, xmm0
  paddw xmm2, xmm1
  movdqu xmmword ptr [rax + 2*rcx], xmm3
  movdqu xmmword ptr [rax + 2*rcx + 16], xmm2
  lea rdx, [rcx + 16]
  cmp rdx, 10000
  jne .LBB4_6
  mov qword ptr [rbp - 8], 10000
  mov rsi, qword ptr [rbp - 16]
  test rsi, rsi
  je .LBB4_5
  add rsi, rsi
  mov rdi, qword ptr [rbp - 24]
  mov edx, 2
  call __rust_dealloc@PLT
.LBB4_5:
  add rsp, 64
  pop rbp
  ret
.LBB4_7:
  movups xmm0, xmmword ptr [rbp - 40]
  movaps xmmword ptr [rbp - 64], xmm0
  movaps xmm0, xmmword ptr [rbp - 64]
  movups xmmword ptr [rbp - 40], xmm0
  lea rdi, [rbp - 48]
  call __rust_oom@PLT
  ud2