Optimization 如何防止Rust基准库优化我的代码?

Optimization 如何防止Rust基准库优化我的代码?,optimization,rust,benchmarking,Optimization,Rust,Benchmarking,我有一个简单的想法,我试图在Rust中进行基准测试。然而,当我使用test::Bencher来测量它时,我试图对比的基本情况是: #![feature(test)] extern crate test; #[cfg(test)] mod tests { use test::black_box; use test::Bencher; const ITERATIONS: usize = 100_000; struct CompoundValue {

我有一个简单的想法,我试图在Rust中进行基准测试。然而,当我使用
test::Bencher
来测量它时,我试图对比的基本情况是:

#![feature(test)]
extern crate test;

#[cfg(test)]
mod tests {

    use test::black_box;
    use test::Bencher;

    const ITERATIONS: usize = 100_000;

    struct CompoundValue {
        pub a: u64,
        pub b: u64,
        pub c: u64,
        pub d: u64,
        pub e: u64,
    }

    #[bench]
    fn bench_in_place(b: &mut Bencher) {
        let mut compound_value = CompoundValue {
            a: 0,
            b: 2,
            c: 0,
            d: 5,
            e: 0,
        };

        let val: &mut CompoundValue = &mut compound_value;

        let result = b.iter(|| {
            let mut f : u64 = black_box(0);
            for _ in 0..ITERATIONS {
                f += val.a + val.b + val.c + val.d + val.e;
            }
            f = black_box(f);
            return f;
        });
        assert_eq!((), result);
    }
}
完全由编译器优化,结果是:

running 1 test
test tests::bench_in_place ... bench:           0 ns/iter (+/- 1)
正如你在要点中所看到的,我尝试采用以下建议:

  • 使用
    test::black_box
    方法对编译器隐藏实现细节
  • 从传递给iter方法的闭包返回计算值

我还可以尝试其他技巧吗?

您的基准测试的问题是,优化器知道您的CompoundValue在基准测试期间是不可变的,因此它可以加强减少循环,从而将其编译为常量


解决方案是在CompoundValue的部分使用test::black_box。或者更好的办法是,试着摆脱循环(除非你想对循环性能进行基准测试),让Bencher.iter(..)完成它的工作。

这里的问题是编译器可以看到每次
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#u APP
对是
黑盒
调用。您可以看到,
iter
循环没有太多功能:
movq
只是将数据从寄存器移动到其他寄存器和堆栈,而
addq
/
decq
只是对一些整数进行加和减

从循环上方看,有
movl$700000,%edx
:这是将常量
700_000
加载到edx寄存器中。。。令人怀疑的是,
700000=ITEARATIONS*(0+2+0+5+0)
。(代码中的其他内容没有那么有趣。)

掩饰这一点的方法是将输入设置为
黑盒
,例如,我可能从编写基准开始,如下所示:

#[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
'd,因此编译器无法预计算加法并在每次调用中重用它

然而,这仍然被优化为非常快:1 ns/iter对我来说。再次检查程序集会发现问题(我已将程序集精简为仅包含
应用程序
/
无应用程序
对的循环,即对
iter
关闭的调用):

现在,编译器已经看到
val
for
循环过程中不会发生变化,因此它正确地将循环转换为只对
val
的所有元素求和(这是4个
addq
的序列),然后将其乘以
迭代次数(
imulq

为了解决这个问题,我们可以做同样的事情:将
黑盒
移得更深,这样编译器就不能对循环的不同迭代之间的值进行推理:

#[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
    });
}
这个版本现在对我来说需要137142 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\u 2:
jne.LBB0\u 2
),以及闭包内部的
for
循环(
.LBB0\u 3:
jne.LBB0\u 3
)。内部循环实际上是在调用
黑盒
应用程序
/
无应用程序
),然后是5个添加项。外部循环将
f
设置为零(
xorl%edi,%edi
),运行内部循环,然后运行
black\u box
ing
f
(第二个
APP
/
无APP


(准确地确定您想要的基准可能很棘手!)

谢谢您的详细解释!
.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