tag recommendation moz metadescription length descriptions description and optimization rust benchmarking

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.