Как я могу помешать библиотеке тестов Rust оптимизировать мой код?

У меня есть простая идея, которую я пытаюсь сравнить с 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 метод.

Есть ли другие хитрости, которые я могу попробовать?

2 ответа

Решение

Проблема здесь в том, что компилятор может видеть, что результат цикла одинаков каждый раз 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 нс /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

Теперь компилятор видел, что val не меняется в течение for цикл, поэтому он правильно преобразовал цикл в суммирование всех элементов val (это последовательность из 4 addqs) и затем умножить это на 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 нс / итер для меня, хотя повторные вызовы 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), запустив внутренний цикл, а затем black_boxИНГ f (второй APP/NO_APP).

(Сравнительный анализ того, что вы хотите сделать, может быть сложным!)

Проблема с вашим тестом состоит в том, что оптимизатор знает, что ваш CompoundValue будет неизменным во время теста, таким образом он может сильно сократить цикл и, таким образом, скомпилировать его до постоянного значения.

Решение состоит в том, чтобы использовать test::black_box на частях вашего CompoundValue. Или, что еще лучше, попытайтесь избавиться от цикла (если только вы не хотите тестировать производительность цикла) и позволить Bencher.iter (..) выполнить свою работу.

Другие вопросы по тегам