Considere este código C:
void foo(void); long bar(long x) { foo(); return x; } Cuando lo compilo en GCC 9.3 con -O3 o -Os , obtengo esto:
bar: push r12 mov r12, rdi call foo mov rax, r12 pop r12 ret La salida de clang es idéntica excepto por la elección de rbx en lugar de r12 como el registro guardado por la persona que llama.
Sin embargo, quiero/espero ver un ensamblaje que se parezca más a esto:
bar: push rdi call foo pop rax ret Dado que tiene que empujar algo a la pila de todos modos, parece más corto, más simple y probablemente más rápido simplemente empujar su valor allí, en lugar de empujar el valor de un registro arbitrario guardado por la persona que llama allí y luego almacenar su valor en ese registro. Lo mismo ocurre con el inverso después de call foo cuando estás volviendo a colocar las cosas.
¿Está mal mi montaje? ¿Es de alguna manera menos eficiente que jugar con un registro adicional? Si la respuesta a ambas es "no", entonces ¿por qué GCC o Clang no lo hacen de esta manera?
Editar: aquí hay un ejemplo menos trivial, para mostrar que sucede incluso si la variable se usa de manera significativa:
long foo(long); long bar(long x) { return foo(x * x) - x; }Entiendo esto:
bar: push rbx mov rbx, rdi imul rdi, rdi call foo sub rax, rbx pop rbx retPrefiero tener esto:
bar: push rdi imul rdi, rdi call foo pop rdi sub rax, rdi retEsta vez, es solo una instrucción frente a dos, pero el concepto central es el mismo.
¿Por qué los compiladores insisten en usar un registro guardado por destinatario aquí?
Porque la mayoría de los compiladores generarían casi el mismo código para una función determinada y siguen convenciones de llamadas globales definidas por la ABI objetivo de su compilador.
Puede definir sus propias convenciones de llamada diferentes (por ejemplo, pasar incluso más argumentos de función en los registros del procesador o, por el contrario, "empaquetar" mediante operaciones bit a bit dos argumentos short en un solo registro del procesador, etc.), e implementar su compilador siguiéndolos . Probablemente necesite volver a codificar parte de la biblioteca estándar de C (por ejemplo, parchear las partes inferiores de GNU libc y luego volver a compilarla, si está en Linux).
IIRC, algunas convenciones de llamadas son diferentes en Windows y en FreeBSD y en Linux para la misma CPU.
Tenga en cuenta que con un GCC reciente (por ejemplo, GCC 10 a principios de 2021) podría compilar y vincular con gcc -O3 -flto -fwhole-program y, en algunos casos, obtener alguna expansión en línea . También puede compilar GCC a partir de su código fuente como un compilador cruzado , y dado que GCC es un software gratuito , puede mejorarlo para que siga sus nuevas convenciones de llamadas privadas. Asegúrese de documentar primero sus convenciones de llamadas.
Si el rendimiento le importa mucho, puede considerar escribir su propio complemento GCC haciendo aún más optimizaciones. Su complemento de compilación podría incluso implementar otras convenciones de llamada (por ejemplo, usando asmjit ).
Considere también mejorar TinyCC o Clang o NWCC para satisfacer sus necesidades.
Mi opinión es que en muchos casos no vale la pena dedicar meses de esfuerzo a mejorar el rendimiento en unos pocos nanosegundos. Pero su empleador/gerente/cliente podría no estar de acuerdo. Considere también compilar (o refactorizar) partes significativas de su software a silicio, por ejemplo, a través de VHDL , o usar hardware especializado, por ejemplo, GPGPU con OpenCL o CUDA .