optimization - recommendation - ¿Cómo puedo evitar que la biblioteca de referencia de Rust optimice mi código?
seo meta descriptions (2)
Tengo una idea simple que estoy tratando de comparar en Rust. Sin embargo, cuando voy a medirlo usando test::Bencher
, el caso base con el que estoy tratando de comparar:
#![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);
}
}
está optimizado completamente por el compilador, lo que resulta en:
running 1 test
test tests::bench_in_place ... bench: 0 ns/iter (+/- 1)
Como puede ver en la esencia, he tratado de emplear las sugerencias establecidas en la documentación , a saber:
- Haciendo uso del método
test::black_box
para ocultar detalles de implementación del compilador. - Devolviendo el valor calculado del cierre pasado al método
iter
.
¿Hay algún otro truco que pueda probar?
El problema aquí es que el compilador puede ver que el resultado del ciclo es el mismo cada vez que iter
llama al cierre (solo agrega alguna constante a f
) porque val
nunca cambia.
Ver el ensamblaje (al pasar --emit asm
al compilador) demuestra esto:
_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
La sección entre .LBB0_2:
y jne .LBB0_2
es a lo que se compila la llamada a iter
, y ejecuta el código en el cierre que le pasa repetidamente. Los pares #APP
#NO_APP
son llamadas black_box
. Puede ver que el ciclo iter
no hace mucho: movq
simplemente está moviendo datos desde el registro hacia / desde otros registros y la pila, y addq
/ decq
solo están agregando y disminuyendo algunos enteros.
Mirando por encima de ese bucle, hay movl $700000, %edx
: Esto está cargando la constante 700_000
en el registro de edx ... y, sospechosamente, 700000 = ITEARATIONS * (0 + 2 + 0 + 5 + 0)
. (Las otras cosas en el código no son tan interesantes).
La forma de disfrazar esto es black_box
la entrada, por ejemplo, podría comenzar con el punto de referencia escrito como:
#[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
});
}
En particular, val
es black_box
''d dentro del cierre, por lo que el compilador no puede black_box
la adición y reutilizarla para cada llamada.
Sin embargo, esto todavía está optimizado para ser muy rápido: 1 ns / iter para mí. Comprobar nuevamente el conjunto revela el problema (he recortado el conjunto hasta el ciclo que contiene los pares APP
/ NO_APP
, es decir, las llamadas al cierre de 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
Ahora el compilador ha visto que val
no cambia en el transcurso del bucle for
, por lo que ha transformado correctamente el bucle en solo sumando todos los elementos de val
(esa es la secuencia de 4 addq
s) y luego multiplicando eso por ITERATIONS
( el imulq
).
Para solucionar esto, podemos hacer lo mismo: mover el black_box
más profundo, para que el compilador no pueda razonar sobre el valor entre las diferentes iteraciones del ciclo:
#[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
});
}
Esta versión ahora toma 137,142 ns / iter para mí, aunque las llamadas repetidas a black_box
probablemente causen una sobrecarga no trivial (tener que escribir varias veces en la pila, y luego volver a leerla).
Podemos mirar el asm, solo para estar seguros:
.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
Ahora la llamada a iter
es dos bucles: el bucle externo que llama al cierre muchas veces ( .LBB0_2:
a jne .LBB0_2
), y el bucle for
dentro del cierre ( .LBB0_3:
a jne .LBB0_3
). El ciclo interno está haciendo una llamada a black_box
( APP
/ NO_APP
) seguido de 5 adiciones. El bucle externo establece f
a cero ( xorl %edi, %edi
), ejecuta el bucle interno y luego black_box
ing f
(la segunda APP
/ NO_APP
).
(¡El benchmarking exactamente lo que quiere comparar puede ser complicado!)
El problema con su punto de referencia es que el optimizador sabe que su CompoundValue va a ser inmutable durante el benchmark, por lo que puede reducir el ciclo y así compilarlo hasta un valor constante.
La solución es usar test :: black_box en las partes de su CompoundValue. O mejor aún, intente deshacerse del bucle (a menos que desee comparar el rendimiento del bucle), y deje que Bencher.iter (..) lo haga.