这里的问题在于编译器能够看到循环的结果每次
iter
调用闭包时都相同(只需将某个常量加到
f
中),因为
val
从未改变。
查看汇编代码(通过向编译器传递
--emit asm
参数)可以证明这一点:
_ZN5tests14bench_in_place20h6a2d53fa00d7c649yaaE:
movq %rdi, %r14
leaq 40(%rsp), %rdi
callq _ZN3sys4time5inner10SteadyTime3now20had09d1fa7ded8f25mjwE@PLT
movq (%r14), %rax
testq %rax, %rax
je .LBB0_3
leaq 24(%rsp), %rcx
movl $700000, %edx
.LBB0_2:
movq $0, 24(%rsp)
#APP
#NO_APP
movq 24(%rsp), %rsi
addq %rdx, %rsi
movq %rsi, 24(%rsp)
#APP
#NO_APP
movq 24(%rsp), %rsi
movq %rsi, 24(%rsp)
#APP
#NO_APP
decq %rax
jne .LBB0_2
.LBB0_3:
leaq 24(%rsp), %rbx
movq %rbx, %rdi
callq _ZN3sys4time5inner10SteadyTime3now20had09d1fa7ded8f25mjwE@PLT
leaq 8(%rsp), %rdi
leaq 40(%rsp), %rdx
movq %rbx, %rsi
callq _ZN3sys4time5inner30_$RF$$u27$a$u20$SteadyTime.Sub3sub20h940fd3596b83a3c25kwE@PLT
movups 8(%rsp), %xmm0
movups %xmm0, 8(%r14)
addq $56, %rsp
popq %rbx
popq %r14
retq
.LBB0_2:
和
jne .LBB0_2
之间的部分是对
iter
的调用编译成的代码,它会反复运行您传递给它的闭包中的代码。
#APP
#NO_APP
对是
black_box
调用。 您可以看到
iter
循环并没有做太多事情:
movq
只是在寄存器之间以及堆栈之间移动数据,而
addq
/
decq
只是在一些整数上进行加法和减法。
在循环上面看到了movl $700000, %edx
:这将常量700_000
加载到edx寄存器中... 令人怀疑的是,700000 = ITERATIONS * (0 + 2 + 0 + 5 + 0)
。(代码中的其他内容并不那么有趣。)
掩盖这一点的方法是black_box
输入,例如,我可以像这样编写基准:
#[bench]
fn bench_in_place(b: &mut Bencher) {
let mut compound_value = CompoundValue {
a: 0,
b: 2,
c: 0,
d: 5,
e: 0,
};
b.iter(|| {
let mut f : u64 = 0;
let val = black_box(&mut compound_value);
for _ in 0..ITERATIONS {
f += val.a + val.b + val.c + val.d + val.e;
}
f
});
}
特别的,在闭包内,
val
被
black_box
了,这样编译器就不能为每个调用预先计算加法并重复使用了。
然而,这仍然被优化得非常快:对我来说是1ns/iter。 再次检查汇编代码揭示了问题(我已经将汇编代码缩减到仅包含 APP
/NO_APP
对的循环中,即对 iter
的闭包的调用):
.LBB0_2:
movq %rcx, 56(%rsp)
movq 56(%rsp), %rsi
movq 8(%rsi), %rdi
addq (%rsi), %rdi
addq 16(%rsi), %rdi
addq 24(%rsi), %rdi
addq 32(%rsi), %rdi
imulq $100000, %rdi, %rsi
movq %rsi, 56(%rsp)
decq %rax
jne .LBB0_2
现在编译器已经看到
val
在整个
for
循环中没有改变,因此它正确地将循环转换为仅对
val
的所有元素求和(这是4个
addq
序列),然后将其乘以
ITERATIONS
(即
imulq
)。
要解决这个问题,我们可以做同样的事情:将
black_box
深入移动,使得编译器无法推断出在循环的不同迭代之间的值。
#[bench]
fn bench_in_place(b: &mut Bencher) {
let mut compound_value = CompoundValue {
a: 0,
b: 2,
c: 0,
d: 5,
e: 0,
};
b.iter(|| {
let mut f : u64 = 0;
for _ in 0..ITERATIONS {
let val = black_box(&mut compound_value);
f += val.a + val.b + val.c + val.d + val.e;
}
f
});
}
这个版本现在对我来说需要137,142 ns/iter,尽管对black_box
的重复调用可能会造成非常重要的开销(需要重复写入堆栈,然后再读取它)。
我们可以查看asm,以确保:
.LBB0_2:
movl $100000, %ebx
xorl %edi, %edi
.align 16, 0x90
.LBB0_3:
movq %rdx, 56(%rsp)
#APP
#NO_APP
movq 56(%rsp), %rax
addq (%rax), %rdi
addq 8(%rax), %rdi
addq 16(%rax), %rdi
addq 24(%rax), %rdi
addq 32(%rax), %rdi
decq %rbx
jne .LBB0_3
incq %rcx
movq %rdi, 56(%rsp)
#APP
#NO_APP
cmpq %r8, %rcx
jne .LBB0_2
现在对iter的调用是两个循环:外部循环多次调用闭包(从.LBB0_2到jne .LBB0_2),闭包内部的for循环(从.LBB0_3到jne .LBB0_3)。内部循环确实执行了对black_box的调用(APP/NO_APP),然后进行了5次加法。外部循环将f设置为零(xorl %edi, %edi),运行内部循环,然后对f进行black_box处理(第二个APP/NO_APP)。
(准确基准测试需要技巧!)