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

时间:2015-09-03 21:35:02

标签: 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 {
        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)

正如您在要点中所看到的,我尝试使用set forth in the documentation的建议,即:

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

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

2 个答案:

答案 0 :(得分:3)

这里的问题是编译器可以看到每次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 / {{1}只是添加和减少一些整数。

在上面的循环中,有decq:这会将常量movl $700000, %edx加载到edx寄存器中......并且可疑地700_000。 (代码中的其他内容并不那么有趣。)

伪装此方法的方法是700000 = ITEARATIONS * (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,因此编译器无法预先计算添加内容并为每次调用重复使用。

然而,这仍然是非常快的优化:1 ns / iter对我来说。再次检查装配体会显示问题(我已将装配体修剪成仅包含black_box / APP对的循环,即调用NO_APP'关闭):

iter

现在编译器已经看到.LBB0_2: movq %rcx, 56(%rsp) #APP #NO_APP 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) #APP #NO_APP decq %rax jne .LBB0_2 val循环过程中没有发生变化,所以它已经正确地将循环转换为只汇总{{1}的所有元素(那是4 for s的序列),然后乘以valaddq)。

要解决这个问题,我们可以做同样的事情:更深入地移动ITERATIONS,这样编译器就无法推断循环的不同迭代之间的值:

imulq

这个版本现在需要137,142 ns / iter,虽然重复调用black_box可能会导致非平凡的开销(不得不重复写入堆栈,然后再读回来)。

我们可以看看asm,只是为了确定:

#[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
    });
}

现在对black_box的调用是两个循环:多次调用闭包的外循环(.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_2for)。内部循环确实调用.LBB0_3:jne .LBB0_3 / black_box),然后添加5次。外部循环将APP设置为零(NO_APP),运行内部循环,然后f xorl %edi, %edi {第二black_box / {{1 }})。

(准确对基准进行基准测试可能会非常棘手!)

答案 1 :(得分:0)

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

解决方案是在CompoundValue的部分使用test :: black_box。或者甚至更好,尝试摆脱循环(除非你想要基准测试循环性能),让Bencher.iter(..)做它的工作。