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




benchmarking (2)

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

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

我有一个简单的想法,我试图在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方法。

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


这里的问题是编译器可以看到,每次iter调用闭包时,循环的结果都是一样的(只是给f添加一些常量),因为val永远不会改变。

查看程序集(通过传递 - 发送到编译器的--emit asm )演示了这一点:

_ZN5tests14bench_in_place20h6a2d53fa00d7c649yaaE:
    ; ...
    movq    %rdi, %r14
    leaq    40(%rsp), %rdi
    callq   [email protected]
    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   [email protected]
    leaq    8(%rsp), %rdi
    leaq    40(%rsp), %rdx
    movq    %rbx, %rsi
    callq   _ZN3sys4time5inner30_$RF$$u27$a$u20$SteadyTime.[email protected]
    movups  8(%rsp), %xmm0
    movups  %xmm0, 8(%r14)
    addq    $56, %rsp
    popq    %rbx
    popq    %r14
    retq

.LBB0_2:jne .LBB0_2之间的部分是.LBB0_2:的调用编译的内容,它重复地运行你传递给它的闭包中的代码。 #APP #NO_APP对是black_box调用。 你可以看到, iter循环并没有做太多的事情: movq只是将数据从寄存器移动到其他寄存器或从其他寄存器中addq ,而addq / decq只是增加和减少一些整数。

看上面那个循环,有movl $700000, %edx :这是加载常数700_000到edx寄存器...,并且可疑地, 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在关闭中是black_box ,所以编译器不能预先计算添加,并在每次调用时重新使用它。

然而,这仍然是非常快的优化:1 ns / iter对我来说。 再次检查程序集会发现问题(我已经将程序集修剪成仅包含APP / NO_APP对的循环,即调用它的闭包):

.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

现在编译器已经看到valfor循环过程中不会改变,所以它已经正确地将循环转换成只是将val所有元素(即4个addq的序列) addq ,然后再乘以ITERATIONSimulq )。

为了解决这个问题,我们可以做同样的事情:更深的移动black_box ,以便编译器不能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_boxAPP / NO_APP ),然后再添加5个。 外循环将f设置为零( xorl %edi, %edi ),运行内循环,然后将black_box f (第二个APP / NO_APP )。

(准确地标定你想要的基准是非常棘手的!)







benchmarking