¿Cómo eliminar el "ruido" de la salida del ensamblaje GCC/clang?
Quiero inspeccionar el resultado del ensamblaje de la aplicación.boost::variant
en mi código para ver qué llamadas intermedias están optimizadas.
Cuando compilo el siguiente ejemplo (con GCC 5.3 usando g++ -O3 -std=c++14 -S
), parece como si el compilador optimizara todo y devolviera directamente 100:
(...)
main:
.LFB9320:
.cfi_startproc
movl $100, %eax
ret
.cfi_endproc
(...)
#include <boost/variant.hpp>
struct Foo
{
int get() { return 100; }
};
struct Bar
{
int get() { return 999; }
};
using Variant = boost::variant<Foo, Bar>;
int run(Variant v)
{
return boost::apply_visitor([](auto& x){return x.get();}, v);
}
int main()
{
Foo f;
return run(f);
}
Sin embargo, el resultado completo del ensamblado contiene mucho más que el extracto anterior, que a mí me parece que nunca se llama. ¿Hay alguna manera de decirle a GCC/clang que elimine todo ese "ruido" y simplemente genere lo que realmente se llama cuando se ejecuta el programa?
salida de ensamblaje completo:
.file "main1.cpp"
.section .rodata.str1.8,"aMS",@progbits,1
.align 8
.LC0:
.string "/opt/boost/include/boost/variant/detail/forced_return.hpp"
.section .rodata.str1.1,"aMS",@progbits,1
.LC1:
.string "false"
.section .text.unlikely._ZN5boost6detail7variant13forced_returnIvEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIvEET_v,comdat
.LCOLDB2:
.section .text._ZN5boost6detail7variant13forced_returnIvEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIvEET_v,comdat
.LHOTB2:
.p2align 4,,15
.weak _ZN5boost6detail7variant13forced_returnIvEET_v
.type _ZN5boost6detail7variant13forced_returnIvEET_v, @function
_ZN5boost6detail7variant13forced_returnIvEET_v:
.LFB1197:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $_ZZN5boost6detail7variant13forced_returnIvEET_vE19__PRETTY_FUNCTION__, %ecx
movl $49, %edx
movl $.LC0, %esi
movl $.LC1, %edi
call __assert_fail
.cfi_endproc
.LFE1197:
.size _ZN5boost6detail7variant13forced_returnIvEET_v, .-_ZN5boost6detail7variant13forced_returnIvEET_v
.section .text.unlikely._ZN5boost6detail7variant13forced_returnIvEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIvEET_v,comdat
.LCOLDE2:
.section .text._ZN5boost6detail7variant13forced_returnIvEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIvEET_v,comdat
.LHOTE2:
.section .text.unlikely._ZN5boost6detail7variant13forced_returnIiEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIiEET_v,comdat
.LCOLDB3:
.section .text._ZN5boost6detail7variant13forced_returnIiEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIiEET_v,comdat
.LHOTB3:
.p2align 4,,15
.weak _ZN5boost6detail7variant13forced_returnIiEET_v
.type _ZN5boost6detail7variant13forced_returnIiEET_v, @function
_ZN5boost6detail7variant13forced_returnIiEET_v:
.LFB9757:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $_ZZN5boost6detail7variant13forced_returnIiEET_vE19__PRETTY_FUNCTION__, %ecx
movl $39, %edx
movl $.LC0, %esi
movl $.LC1, %edi
call __assert_fail
.cfi_endproc
.LFE9757:
.size _ZN5boost6detail7variant13forced_returnIiEET_v, .-_ZN5boost6detail7variant13forced_returnIiEET_v
.section .text.unlikely._ZN5boost6detail7variant13forced_returnIiEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIiEET_v,comdat
.LCOLDE3:
.section .text._ZN5boost6detail7variant13forced_returnIiEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIiEET_v,comdat
.LHOTE3:
.section .text.unlikely,"ax",@progbits
.LCOLDB4:
.text
.LHOTB4:
.p2align 4,,15
.globl _Z3runN5boost7variantI3FooJ3BarEEE
.type _Z3runN5boost7variantI3FooJ3BarEEE, @function
_Z3runN5boost7variantI3FooJ3BarEEE:
.LFB9310:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl (%rdi), %eax
cltd
xorl %edx, %eax
cmpl $19, %eax
ja .L7
jmp *.L9(,%rax,8)
.section .rodata
.align 8
.align 4
.L9:
.quad .L30
.quad .L10
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.quad .L7
.text
.p2align 4,,10
.p2align 3
.L7:
call _ZN5boost6detail7variant13forced_returnIiEET_v
.p2align 4,,10
.p2align 3
.L30:
movl $100, %eax
.L8:
addq $8, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.p2align 4,,10
.p2align 3
.L10:
.cfi_restore_state
movl $999, %eax
jmp .L8
.cfi_endproc
.LFE9310:
.size _Z3runN5boost7variantI3FooJ3BarEEE, .-_Z3runN5boost7variantI3FooJ3BarEEE
.section .text.unlikely
.LCOLDE4:
.text
.LHOTE4:
.globl _Z3runN5boost7variantI3FooI3BarEEE
.set _Z3runN5boost7variantI3FooI3BarEEE,_Z3runN5boost7variantI3FooJ3BarEEE
.section .text.unlikely
.LCOLDB5:
.section .text.startup,"ax",@progbits
.LHOTB5:
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB9320:
.cfi_startproc
movl $100, %eax
ret
.cfi_endproc
.LFE9320:
.size main, .-main
.section .text.unlikely
.LCOLDE5:
.section .text.startup
.LHOTE5:
.section .rodata
.align 32
.type _ZZN5boost6detail7variant13forced_returnIvEET_vE19__PRETTY_FUNCTION__, @object
.size _ZZN5boost6detail7variant13forced_returnIvEET_vE19__PRETTY_FUNCTION__, 58
_ZZN5boost6detail7variant13forced_returnIvEET_vE19__PRETTY_FUNCTION__:
.string "T boost::detail::variant::forced_return() [with T = void]"
.align 32
.type _ZZN5boost6detail7variant13forced_returnIiEET_vE19__PRETTY_FUNCTION__, @object
.size _ZZN5boost6detail7variant13forced_returnIiEET_vE19__PRETTY_FUNCTION__, 57
_ZZN5boost6detail7variant13forced_returnIiEET_vE19__PRETTY_FUNCTION__:
.string "T boost::detail::variant::forced_return() [with T = int]"
.ident "GCC: (Ubuntu 5.3.0-3ubuntu1~14.04) 5.3.0 20151204"
.section .note.GNU-stack,"",@progbits
Eliminar las .cfi
directivas, las etiquetas no utilizadas y las líneas de comentarios es un problema resuelto: los scripts detrás del explorador de compiladores de Matt Godbolt son de código abierto en su proyecto github . Incluso puede resaltar colores para hacer coincidir las líneas de origen con las líneas de ensamblaje (usando la información de depuración).
Puede configurarlo localmente para poder alimentarlo con archivos que forman parte de su proyecto con todas las #include
rutas, etc. (usando -I/...
). Y entonces puedes usarlo en código fuente privado que no deseas enviar a través de Internet.
Charla CppCon2017 de Matt Godbolt “¿Qué ha hecho mi compilador por mí últimamente? Unbolting the Compiler's Lid” muestra cómo usarlo (se explica por sí mismo, pero tiene algunas características interesantes si lees los documentos en github), y también cómo leer x86 asm , con una suave introducción al propio x86 asm para principiantes. y mirar la salida del compilador. Continúa mostrando algunas optimizaciones interesantes del compilador (por ejemplo, para dividir por una constante) y qué tipo de funciones dan resultados ASM útiles para observar los resultados optimizados del compilador (argumentos de función, no int a = 123;
).
En el explorador del compilador Godbolt, puede ser útil usarlo -g0 -fno-asynchronous-unwind-tables
si desea desmarcar la opción de filtro para directivas, por ejemplo, porque desea ver .section
y .p2align
cosas en la salida del compilador. El valor predeterminado es agregar -g
a sus opciones para obtener la información de depuración que utiliza para resaltar con colores las líneas fuente y de ensamblaje coincidentes, pero esto significa .cfi
directivas para cada operación de pila y .loc
para cada línea fuente, entre otras cosas.
Con gcc/clang simple (no g++), -fno-asynchronous-unwind-tables
evita .cfi
directivas. Posiblemente también sea útil: -fno-exceptions -fno-rtti
-masm=intel
. Asegúrate de omitir -g
.
Copie/pegue esto para uso local :
g++ -fno-asynchronous-unwind-tables -fno-exceptions -fno-rtti -fverbose-asm \
-Wall -Wextra foo.cpp -O3 -masm=intel -S -o- | less
O -Os
puede ser más legible, por ejemplo, usar div
para la división por constantes que no son potencias de 2 en lugar de un inverso multiplicativo, aunque eso es mucho peor para el rendimiento y solo un poco más pequeño, en todo caso.
En Godbolt, utilícelo-g0
si desea desmarcar "directivas de filtro" para ver secciones y demás, o variables globales que filtró incorrectamente. Su valor predeterminado -g
puede desordenar las cosas, por lo que -g0
lo contrarresta. ( -g0
es el valor predeterminado sin opciones de línea de comandos). Utilice también -fno-asynchronous-unwind-tables
, tal vez también -fno-exceptions -fno-rtti
.
¡Pero en realidad, recomendaría usar Godbolt directamente (en línea o configurarlo localmente)! Puede cambiar rápidamente entre versiones de gcc y clang para ver si los compiladores nuevos o antiguos hacen algo tonto. (O lo que hace ICC, o incluso lo que hace MSVC). Incluso hay ARM / ARM64 gcc 6.3 y varios gcc para PowerPC, MIPS, AVR, MSP430. (Puede ser interesante ver qué sucede en una máquina que int
es más ancha que un registro o que no es de 32 bits. O en un RISC frente a x86).
Para C en lugar de C++, puede utilizar -xc -std=gnu11
para evitar cambiar el menú desplegable de idioma a C, lo que restablece el panel de origen y las opciones del compilador, y tiene un conjunto diferente de compiladores disponibles.
Opciones de compilación útiles para crear ensamblaje para consumo humano :
Recuerde, su código solo tiene que compilarse, no vincularse: pasar un puntero a una función externa como
void ext(void*p)
es una buena manera de evitar que algo se optimice . Solo necesita un prototipo, sin definición, por lo que el compilador no puede incluirlo ni hacer suposiciones sobre lo que hace. (O el tipo asm en líneaBenchmark::DoNotOptimize
puede obligar a un compilador a materializar un valor en un registro, u olvidarse de que sea una constante conocida, si conoce la sintaxis asm en línea de GNU C lo suficientemente bien como para usar restricciones para comprender el efecto que está teniendo en lo que están exigiendo al compilador.)Recomiendo usarlo
-O3 -Wall -Wextra -fverbose-asm -march=haswell
para mirar el código. (-fverbose-asm
Sin embargo, puede hacer que la fuente parezca ruidosa cuando todo lo que obtiene son temporales numerados como nombres para los operandos). Cuando esté jugueteando con la fuente para ver cómo cambia el conjunto, definitivamente querrá habilitar las advertencias del compilador. No querrás perder el tiempo rascándote la cabeza cuando la explicación es que hiciste algo que merece una advertencia en la fuente.Para ver cómo funciona la convención de llamadas, a menudo conviene observar a la persona que llama y al destinatario sin incluirlas .
Puede usarlo
__attribute__((noipa)) foo_t foo(bar_t x) { ... }
en una definición o compilar congcc -O3 -fno-inline-functions -fno-inline-functions-called-once -fno-inline-small-functions
para deshabilitar la inserción. (Pero esas opciones de línea de comando no deshabilitan la clonación de una función para propagación constante.noipa
= no hay análisis entre procedimientos. Es incluso más fuerte que__attribute__((noinline,noclone))
.) Consulte Desde la perspectiva del compilador, cómo se maneja la referencia para la matriz y por qué pasar por valor. (no descomposición) ¿no está permitido? para un ejemplo.O si simplemente desea ver cómo las funciones pasan/reciben argumentos de diferentes tipos, puede usar nombres diferentes pero el mismo prototipo para que el compilador no tenga una definición para incorporar. Esto funciona con cualquier compilador. Sin una definición, una función es solo un cuadro negro para el optimizador, gobernado únicamente por la convención de llamada/ABI.
-ffast-math
obtendrá muchas funciones libm en línea, algunas en una sola instrucción (especialmente con SSE4 disponible pararoundsd
). Algunos se alinearán solo con-fno-math-errno
, u otras partes "más seguras" de-ffast-math
, sin las partes que permiten al compilador redondear de manera diferente. Si tiene código FP, definitivamente mírelo con/sin-ffast-math
. Si no puede habilitar de forma segura ninguno de-ffast-math
en su compilación habitual, tal vez tenga una idea de un cambio seguro que pueda realizar en el código fuente para permitir la misma optimización sin-ffast-math
.-O3 -fno-tree-vectorize
se optimizará sin auto-vectorización , por lo que puede obtener una optimización completa sin si desea comparar-O2
(que no habilita la autovectorización en gcc11 y versiones anteriores, pero sí en todos los clang).-Os
(optimizar tamaño y velocidad) puede resultar útil para mantener el código más compacto, lo que significa menos código para comprender. clang-Oz
optimiza el tamaño incluso cuando perjudica la velocidad, incluso usandopush 1
/pop rax
en lugar demov eax, 1
, por lo que solo es interesante para code golf .Incluso
-Og
(optimización mínima) podría ser lo que quieras ver, dependiendo de tus objetivos.-O0
está lleno de ruido de almacenamiento/recarga, lo que hace que sea más difícil de seguir, a menos que usesregister
vars . La única ventaja es que cada instrucción C se compila en un bloque de instrucciones separado y permite-fverbose-asm
usar los nombres reales de las var C.clang desenrolla bucles de forma predeterminada, por lo que
-fno-unroll-loops
puede resultar útil en funciones complejas . Puede tener una idea de "lo que hizo el compilador" sin tener que recorrer los bucles desenrollados. (gcc habilita-funroll-loops
con-fprofile-use
, pero no con-O3
). (Esta es una sugerencia para código legible por humanos, no para código que se ejecutaría más rápido).Definitivamente habilite algún nivel de optimización, a menos que desee saber específicamente qué
-O0
hizo . Su requisito de "comportamiento de depuración predecible" hace que el compilador almacene/recargue todo entre cada instrucción C, por lo que puede modificar las variables C con un depurador e incluso "saltar" a una línea fuente diferente dentro de la misma función, y hacer que la ejecución continúe como si Hice eso en la fuente C.-O0
la salida es muy ruidosa con las tiendas/recargas (y muy lenta) no solo por la falta de optimización, sino también por la desoptimización forzada para admitir la depuración . (también relacionado ).
Para obtener una combinación de fuente y asm , use gcc -Wa,-adhln -c -g foo.c | less
para pasar opciones adicionales a as
. (Más discusión sobre esto en una publicación de blog y en otro blog ). Tenga en cuenta que la salida de esto no es una entrada válida del ensamblador, porque la fuente C está ahí directamente, no como un comentario del ensamblador. Así que no lo llames .s
. A .lst
podría tener sentido si desea guardarlo en un archivo.
El resaltado de color de Godbolt tiene un propósito similar y es excelente para ayudarlo a ver cuando varias instrucciones ensambladas no contiguas provienen de la misma línea fuente. No he usado ese comando de listado de gcc en absoluto, así que IDK qué tan bien funciona y qué fácil es para el ojo verlo, en ese caso.
Me gusta la alta densidad de código del panel de ensamblaje de Godbolt, así que no creo que me guste tener líneas fuente mezcladas. Al menos no para funciones simples. Tal vez con una función que era demasiado compleja para entender la estructura general de lo que hace el conjunto...
Y recuerde, cuando quiera simplemente mirar el conjunto, omita las main()
constantes de tiempo de compilación y . Desea ver el código para tratar con una función arg en un registro, no el código después de que la propagación constante lo convierte en return 42
, o al menos optimiza algunas cosas.
Quitar static
y/o inline
de funciones producirá una definición independiente para ellas, así como una definición para cualquier persona que llame, así que puedes mirar eso.
No pongas tu código en una función llamadamain()
. gcc sabe que main
es especial y asume que solo se llamará una vez, por lo que lo marca como "frío" y lo optimiza menos.
La otra cosa que puedes hacer: si creaste un archivo main()
, puedes ejecutarlo y usar un depurador. stepi
(si
) pasos por instrucción. Ver la parte inferior delx86 etiqueta wiki para obtener instrucciones. Pero recuerde que el código podría optimizarse después de insertarlo en main con argumentos constantes en tiempo de compilación.
__attribute__((noinline))
puede ayudar, en una función que no desea incluir. gcc también hará clones de funciones de propagación constante, es decir, una versión especial con uno de los argumentos como constante, para sitios de llamadas que saben que están pasando una constante. El nombre del símbolo será .clone.foo.constprop_1234
o algo así en la salida del asm. También puedes usar __attribute__((noclone))
para desactivar eso).
Por ejemplo
Si desea ver cómo el compilador multiplica dos números enteros: puse el siguiente código en el explorador del compilador Godbolt para obtener el asm (de gcc -O3 -march=haswell -fverbose-asm
) de la manera incorrecta y la forma correcta de probar esto.
// the wrong way, which people often write when they're used to creating a runnable test-case with a main() and a printf
// or worse, people will actually look at the asm for such a main()
int constants() { int a = 10, b = 20; return a * b; }
mov eax, 200 #,
ret # compiles the same as return 200; not interesting
// the right way: compiler doesn't know anything about the inputs
// so we get asm like what would happen when this inlines into a bigger function.
int variables(int a, int b) { return a * b; }
mov eax, edi # D.2345, a
imul eax, esi # D.2345, b
ret
(Esta combinación de asm y C se creó a mano copiando y pegando la salida de asm de godbolt en el lugar correcto. Creo que es una buena manera de mostrar cómo se compila una función breve en respuestas SO/informes de errores del compilador/correos electrónicos).
Siempre puede ver el ensamblado generado desde el archivo objeto, en lugar de utilizar la salida del ensamblado del compilador. objdump
me viene a la mente.
Incluso puede indicar objdump
que se mezcle la fuente con el ensamblaje, lo que facilita determinar qué línea de fuente corresponde a qué instrucciones. Sesión de ejemplo:
$ cat test.cc
int foo(int arg)
{
return arg * 42;
}
$ g++ -g -O3 -std=c++14 -c test.cc -o test.o && objdump -dS -M intel test.o
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z3fooi>:
int foo(int arg)
{
return arg + 1;
0: 8d 47 01 lea eax,[rdi+0x1]
}
3: c3 ret
Explicación de objdump
banderas:
-d
desmonta todas las secciones ejecutables-S
mezcla el ensamblaje con la fuente (-g
requerido al compilar cong++
)-M intel
elige la sintaxis de Intel en lugar de la fea sintaxis de AT&T ( opcional )
Me gusta insertar etiquetas que pueda extraer fácilmente de la salida del objdump.
int main() {
asm volatile ("interesting_part_begin%=:":);
do_something();
asm volatile ("interesting_part_end%=:":);
}
No he tenido ningún problema con esto todavía, pero asm volatile
puede ser muy difícil para el optimizador de un compilador porque tiende a dejar ese código intacto.