c gcc x86 inline-assembly red-zone

c - Montaje en línea que golpea la zona roja.



gcc x86 (5)

Estoy escribiendo un programa de criptografía, y el núcleo (una amplia rutina de multiplicación) está escrito en el ensamblaje x86-64, tanto para la velocidad como para usar extensivamente instrucciones como adc que no son fácilmente accesibles desde C. No quiero en línea esta función, porque es grande y se llama varias veces en el bucle interno.

Idealmente, también me gustaría definir una convención de llamada personalizada para esta función, ya que internamente utiliza todos los registros (excepto rsp ), no obstruye sus argumentos y regresa en los registros. En este momento, está adaptado a la convención de llamadas C, pero, por supuesto, esto lo hace más lento (alrededor del 10%).

Para evitar esto, puedo llamarlo con asm("call %Pn" : ... : my_function... : "cc", all the registers); ¿pero hay una manera de decirle a GCC que la instrucción de llamada se mete con la pila? De lo contrario, GCC solo colocará todos esos registros en la zona roja, y el superior quedará aplastado. Puedo compilar todo el módulo con -mno-red-zone, pero preferiría una manera de decirle a GCC que, digamos, los 8 bytes superiores de la zona roja serán eliminados para que no coloquen nada allí.


¿No puede simplemente modificar su función de ensamblaje para cumplir con los requisitos de una señal en el ABI x86-64 desplazando el puntero de la pila en 128 bytes al ingresar a su función?

O si se está refiriendo al puntero de retorno en sí mismo, coloque el cambio en su macro de llamada (por lo tanto, sub %rsp; call... )


¿Qué pasa con la creación de una función ficticia que está escrita en C y no hace más que llamar al ensamblaje en línea?


De su pregunta original no me di cuenta del uso limitado de la zona roja de gcc para las funciones de hoja. No creo que lo requiera la ABI x86_64, pero es una suposición de simplificación razonable para un compilador. En ese caso, solo necesita hacer que la función que llama a su rutina de ensamblaje no sea una hoja para fines de compilación:

int global; was_leaf() { if (global) other(); }

GCC no puede saber si el valor global será verdadero, por lo que no puede optimizar la llamada a other() por lo que was_leaf() ya no es una función de hoja. Compilé esto (con más código que activó el uso de la pila) y observé que como hoja no se movió %rsp y con la modificación mostrada, lo hizo.

También intenté simplemente asignar más de 128 bytes (solo char buf[150] ) en una hoja, pero me sorprendió ver que solo hizo una resta parcial:

pushq %rbp movq %rsp, %rbp subq $40, %rsp movb $7, -155(%rbp)

Si vuelvo a colocar el código de derrota de hojas en ese subq $160, %rsp


La forma de máximo rendimiento podría ser escribir todo el bucle interno en asm (incluidas las instrucciones de call , si realmente vale la pena desenrollarlo pero no en línea. Ciertamente es plausible si la inclusión completa está causando demasiados errores de caché uop en otro lugar).

De todos modos, haga que C llame a una función asm que contiene su bucle optimizado.

Por cierto, la eliminación de todos los registros hace que sea difícil para gcc hacer un muy buen bucle, por lo que es posible que salga adelante optimizando el bucle completo. (Por ejemplo, tal vez mantenga un puntero en un registro y un puntero final en la memoria, porque cmp mem,reg todavía es bastante eficiente).

Eche un vistazo al código que gcc / clang envuelve alrededor de una declaración asm que modifica un elemento de matriz (en Godbolt ):

void testloop(long *p, long count) { for (long i = 0 ; i < count ; i++) { asm(" # XXX asm operand in %0" : "+r" (p[i]) : : // "rax", "rbx", "rcx", "rdx", "rdi", "rsi", "rbp", "r8", "r9", "r10", "r11", "r12","r13","r14","r15" ); } } #gcc7.2 -O3 -march=haswell push registers and other function-intro stuff lea rcx, [rdi+rsi*8] ; end-pointer mov rax, rdi mov QWORD PTR [rsp-8], rcx ; store the end-pointer mov QWORD PTR [rsp-16], rdi ; and the start-pointer .L6: # rax holds the current-position pointer on loop entry # also stored in [rsp-16] mov rdx, QWORD PTR [rax] mov rax, rdx # looks like a missed optimization vs. mov rax, [rax], because the asm clobbers rdx XXX asm operand in rax mov rbx, QWORD PTR [rsp-16] # reload the pointer mov QWORD PTR [rbx], rax mov rax, rbx # another weird missed-optimization (lea rax, [rbx+8]) add rax, 8 mov QWORD PTR [rsp-16], rax cmp QWORD PTR [rsp-8], rax jne .L6 # cleanup omitted.

clang cuenta un contador separado hacia abajo hacia cero. Pero usa load / add -1 / store en lugar de una memoria-destino add [mem], -1 / jnz .

Probablemente pueda hacerlo mejor si escribe el bucle completo usted mismo en asm en lugar de dejar esa parte de su bucle activo al compilador.

Considere el uso de algunos registros XMM para la aritmética de enteros para reducir la presión de registro en los registros de enteros, si es posible. En las CPU de Intel, moverse entre los registros GP y XMM solo cuesta 1 ALU uop con 1c de latencia. (Todavía es 1 uop en AMD, pero una latencia más alta, especialmente en la familia Bulldozer). Hacer cosas con enteros escalares en los registros XMM no es mucho peor, y podría valer la pena si el rendimiento total de UO es su cuello de botella, o ahorra más derrames / recargas de lo que cuesta.

Pero, por supuesto, XMM no es muy viable para los contadores de bucle ( paddd / pcmpeq / pmovmskb / cmp / psubd o psubd / ptest / ptest no son muy buenos en comparación con sub [mem], 1 / jcc), o para punteros, o para la extensión de aritmética de precisión (realizar manualmente el arrastre con una comparación y el acarreo con otro paddq apesta, incluso en el modo de 32 bits, donde no están disponibles los registros de enteros de 64 bits). Por lo general, es mejor derramar / recargar en la memoria en lugar de los registros de XMM, si no se encuentra en un cuello de botella en las operaciones de carga / almacenamiento.

Si también necesita llamadas a la función desde fuera del bucle (limpieza o algo así), escriba una envoltura o use add $-128, %rsp ; call ; sub $-128, %rsp add $-128, %rsp ; call ; sub $-128, %rsp add $-128, %rsp ; call ; sub $-128, %rsp para conservar la zona roja en esas versiones. (Tenga en cuenta que -128 es codificable como imm8 pero +128 no lo es).

Sin embargo, la inclusión de una llamada de función real en su función C no necesariamente hace que sea seguro asumir que la zona roja no está en uso. Cualquier derrame / recarga entre las llamadas a la función (visible desde el compilador) podría usar la zona roja, por lo que es muy probable que la supresión de todos los registros en una declaración asm provoque ese comportamiento.

// a non-leaf function that still uses the red-zone with gcc void bar(void) { //cryptofunc(1); // gcc/clang don''t use the redzone after this (not future-proof) volatile int tmp = 1; (void)tmp; cryptofunc(1); // but gcc will use the redzone before a tailcall } # gcc7.2 -O3 output mov edi, 1 mov DWORD PTR [rsp-12], 1 mov eax, DWORD PTR [rsp-12] jmp cryptofunc(long)

Si desea depender del comportamiento específico del compilador, puede llamar (con C normal) una función no en línea antes del ciclo activo. Con el gcc / clang actual, eso les hará reservar suficiente espacio de pila ya que tienen que ajustar la pila de todos modos (para alinear rsp antes de una call ). Esto no está preparado para el futuro, pero debería funcionar.

GNU C tiene un __attribute__((target("options"))) x86 function , pero no es utilizable para opciones arbitrarias , y -mno-redzone no es uno de los que puede alternar en función de cada función, o con #pragma GCC target ("options") dentro de una unidad de compilación.

Puedes usar cosas como

__attribute__(( target("sse4.1,arch=core2") )) void penryn_version(void) { ... }

pero no __attribute__(( target("-mno-redzone") )) .

Hay un #pragma GCC optimize y un atributo de función de optimize (ambos no están diseñados para el código de producción), pero #pragma GCC optimize ("-mno-redzone") no funciona de todos modos. Creo que la idea es permitir que algunas funciones importantes se optimicen con -O2 incluso en versiones de depuración. Puede configurar -f opciones u -O .


No estoy seguro, pero mirando la documentación de GCC para los atributos de la función , encontré el atributo de la función stdcall que podría ser de interés.

Todavía me pregunto qué encontrará problemático con su versión de llamada de asm. Si solo se trata de estética, podría transformarla en una macro o una función en línea.