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