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

标签 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 方法的闭包返回计算值。

我可以尝试其他技巧吗?

最佳答案

这里的问题是编译器可以看到每次 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 = 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 对的循环,即对 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

现在编译器已经看到 valfor 循环的过程中没有改变,所以它正确地将循环转换为只是对所有元素求和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: to jne .LBB0_2 ),以及闭包内的 for 循环(.LBB0_3:jne .LBB0_3)。内部循环确实在调用 black_box (APP/NO_APP),然后进行 5 次添加。外循环将 f 设置为零 (xorl %edi, %edi),运行内循环,然后 black_boxing f(第二个APP/NO_APP)。

(准确地对您想要的基准进行基准测试可能很棘手!)

关于optimization - 如何防止 Rust 基准库优化我的代码?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32385805/

相关文章:

c# - 异步函数可以内联吗?

c++ - 正确地完成了一个类似竞争的问题,但需要帮助来提高其效率

rust - 如何使用 serde_yaml 反序列化结构中的 YAML

linux - 如何在进程中配置和采样英特尔性能计数器

ios - 基准测试 iPhone 应用程序电池使用情况的最佳方法?

javascript - 哪个 javascript 缩小库产生更好的结果?

c++ - 一个空的类会被优化掉吗

python - 在 n 次函数调用后自动停止 scipy.optimize.fmin_bfgs (不是 BFGS 迭代!)

rust - 在 Rust 中使用 iodbc

rust - 不关心拥有子结构的递归数据类型