Как я могу помешать библиотеке тестов 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 addq
s) и затем умножить это на 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 (..) выполнить свою работу.