¿Por qué System V / AMD64 ABI exige una alineación de astackmiento de 16 bytes?

He leído en diferentes lugares que se hace por “razones de rendimiento”, pero todavía me pregunto cuáles son los casos particulares en los que el rendimiento mejora con esta alineación de 16 bytes. O, en cualquier caso, ¿cuáles fueron las razones por las cuales se eligió esto?

editar : estoy pensando que escribí la pregunta de una manera engañosa. No estaba preguntando por qué el procesador hace las cosas más rápido con la memoria alineada de 16 bytes, esto se explica en todas partes en los documentos. Lo que quería saber en cambio, es cómo la alineación forzada de 16 bytes es mejor que simplemente dejar que los progtwigdores alineen la stack ellos mismos cuando sea necesario. Pregunto esto porque, desde mi experiencia con el assembly, la aplicación de la stack tiene dos problemas: solo es útil con menos del 1% del código que se ejecuta (por lo que en el otro 99% es en realidad sobrecarga); y también es una fuente muy común de errores. Entonces me pregunto cómo realmente vale la pena al final. Aunque todavía tengo dudas al respecto, estoy aceptando la respuesta de Peter, ya que contiene la respuesta más detallada a mi pregunta original.

Tenga en cuenta que la versión actual de i386 System V ABI utilizada en Linux también requiere 16-byte stack alignment 1 . Consulte https://sourceforge.net/p/fbc/bugs/659/ para obtener un poco de historia.


SSE2 es una línea de base para x86-64 , y hacer que el ABI sea eficiente para tipos como __m128 , y para la auto-vectorización del comstackdor, fue uno de los objectives del diseño, creo. El ABI tiene que definir cómo se pasan dichos argumentos como argumentos de funciones, o por referencia.

La alineación de 16 bytes a veces es útil para las variables locales en la stack (especialmente las matrices), y garantizar la alineación de 16 bytes significa que los comstackdores pueden obtenerla de forma gratuita siempre que sea útil, incluso si la fuente no lo solicita explícitamente.

Si no se conocía la alineación de la stack con respecto a un límite de 16 bytes, cada función que deseara un local alineado necesitaría un and rsp, -16 , e instrucciones adicionales para guardar / restaurar rsp después de un desplazamiento desconocido a rsp ( 0 o -8 ). por ejemplo, utilizando hasta rbp para un puntero de marco.

Sin AVX, los operandos de fuente de memoria deben estar alineados en 16 bytes. por ejemplo, paddd xmm0, [rsp+rdi] falla si el operando de la memoria está desalineado. Por lo tanto, si no se conoce la alineación, deberá usar movups xmm1, [rsp+rdi] / paddd xmm0, xmm1 , o escribir un prólogo / epílogo de bucle para manejar los elementos desalineados. Para las matrices locales que el comstackdor quiere auto-vectorizar, simplemente puede optar por alinearlas por 16.

También tenga en cuenta que las primeras CPUs x86 (antes Nehalem / Bulldozer) tenían una instrucción de movups que es más lenta que movaps incluso cuando el puntero resulta estar alineado. (es decir, las cargas / almacenamientos no alineados en los datos alineados eran más lentos, así como también evitaban que las cargas se plegaran en una instrucción ALU). (Consulte las guías de optimización de Agner Fog, la guía de microarch y las tablas de instrucciones para obtener más información sobre todo lo anterior).

Estos factores explican por qué una garantía es más útil que simplemente “mantener” la stack alineada. Que se le permita crear código que realmente falle en una stack desalineada permite más oportunidades de optimización.

Las matrices alineadas también aceleran las memcpy vectorizadas de memcpy / strcmp / cualquiera que no puedan asumir la alineación, sino que la strcmp y pueden pasar directamente a sus bucles de vector completo.

De una versión reciente de x86-64 System V ABI (r252) :

Una matriz utiliza la misma alineación que sus elementos, excepto que una variable de matriz local o global de longitud de al menos 16 bytes o una variable de matriz de longitud variable C99 siempre tiene una alineación de al menos 16 bytes. 4

4 El requisito de alineación permite el uso de instrucciones SSE cuando se opera en la matriz. En general, el comstackdor no puede calcular el tamaño de una matriz de longitud variable (VLA), pero se espera que la mayoría de los VLA requieran al menos 16 bytes, por lo que es lógico exigir que los VLA tengan al menos una alineación de 16 bytes.

Esto es un poco agresivo, y sobre todo solo ayuda cuando las funciones que se auto-vectorizan pueden estar en línea, pero generalmente hay otros locales que el comstackdor puede almacenar en cualquier espacio para que no pierda espacio en la stack. Y no pierde las instrucciones siempre que haya una alineación de stack conocida. (Obviamente, los diseñadores de ABI podrían haber dejado esto fuera si hubieran decidido no requerir la alineación de la stack de 16 bytes).


Derrame / recarga de __m128

Por supuesto, hace que sea libre hacer alignas(16) char buf[1024]; u otros casos donde la fuente solicita alineación de 16 bytes.

Y también hay __m128 / __m128d / __m128i . El comstackdor puede no ser capaz de mantener todos los locals vectoriales en registros (por ejemplo, dertwigdos a través de una llamada a función, o no suficientes registros), por lo que necesita ser capaz de dertwigr / recargarlos con movaps , o como un operando fuente de memoria para instrucciones ALU , por razones de eficiencia discutidas anteriormente.

Las cargas / almacenamientos que en realidad están divididos en un límite de línea de caché (64 bytes) tienen penalizaciones de latencia significativas, y también penalizaciones de rendimiento menores en las CPU modernas. La carga necesita datos de 2 líneas de caché separadas, por lo que se requieren dos accesos a la caché. (Y potencialmente 2 errores de caché, pero eso es raro para la memoria de la stack).

Creo que los movups ya tenían ese costo para los vectores en CPUs antiguas donde es costoso, pero todavía apesta. Atravesar un límite de 4k páginas es mucho peor (en CPUs antes de Skylake), con una carga o tienda que toma ~ 100 ciclos si toca bytes en ambos lados de un límite de 4k. (También necesita 2 controles TLB). La alineación natural hace que las divisiones a través de cualquier límite más amplio sean imposibles , por lo que la alineación de 16 bytes era suficiente para todo lo que puedes hacer con SSE2.


max_align_t tiene una alineación de 16 bytes en el sistema V ABI x86-64, debido al long double (10 bytes / 80 bits x87). Se define como acolchado a 16 bytes por alguna razón extraña, a diferencia del código de 32 bits donde sizeof(long double) == 10 . x87 10-byte load / store es bastante lento de todos modos (como 1/3 del rendimiento de carga de double o float en Core2, 1/6 en P4 u 1/8 en K8), pero tal vez las penalizaciones de cache-line y page split fueron tan malo en CPUs antiguas que decidieron definirlo de esa manera. Creo que en las CPU modernas (incluso Core2) recorrer una matriz de long double no sería más lento con 10 bytes empaquetados, ya que fld m80 sería un cuello de botella más grande que una línea de caché dividir cada ~ 6.4 elementos.

En realidad, el ABI se definió antes de que el silicio estuviera disponible para el benchmark on ( back in ~ 2000 ), pero esos números K8 son los mismos que K7 (el modo 32 bits / 64 bits es irrelevante aquí). Hacer long double 16 bytes hace posible copiar uno solo con movaps , aunque no se puede hacer nada con él en los registros XMM. (Excepto manipular el bit de signo con xorps / andps / andps )

Relacionado: esta definición de max_align_t significa que malloc siempre devuelve una memoria alineada de 16 bytes en el código x86-64. Esto le permite salirse con la suya para cargas alineadas SSE como _mm_load_ps , pero dicho código puede romperse cuando se comstack para 32 bits donde alignof(max_align_t) es solo 8. (Use aligned_alloc o lo que sea).


Otros factores ABI incluyen pasar __m128 valores en la stack (después de que xmm0-7 tenga los primeros 8 argumentos float / vector). Tiene sentido requerir la alineación de 16 bytes para los vectores en la memoria, de modo que puedan ser utilizados de manera eficiente por el destinatario, y almacenados de manera eficiente por la persona que llama. Mantener la alineación de la stack de 16 bytes hace que sea fácil para las funciones que necesitan alinear un poco de espacio para pasar arg por 16.

Hay tipos como __m128 que las garantías de ABI tienen una alineación de 16 bytes . Si define un local y toma su dirección, y pasa ese puntero a alguna otra función, ese local debe estar lo suficientemente alineado. Por lo tanto, mantener la alineación de la stack de 16 bytes va de la mano con la alineación de algunos tipos de 16 bytes, lo que obviamente es una buena idea.

En estos días, es agradable que atomic puedan obtener alineación de 16 bytes de forma lock cmpxchg16b , por lo que el lock cmpxchg16b nunca cruza un límite de línea de caché. Para el caso realmente raro donde tienes un local atómico con almacenamiento automático, y le pasas punteros a múltiples hilos …


Nota 1: Linux de 32 bits

No todas las plataformas de 32 bits rompieron la compatibilidad hacia atrás con los binarios existentes y los asm escritos a mano como lo hizo Linux; algunos como i386 NetBSD todavía usan el requisito de alineación histórica de 4 bytes de la versión original del i386 SysV ABI.

La alineación histórica de la stack de 4 bytes también fue insuficiente para un eficiente double 8 bytes en las CPU modernas. Los fld / fstp no fstp son generalmente eficientes, excepto cuando cruzan un límite de la línea de caché (como otras cargas / tiendas), por lo que no es horrible, pero la alineación natural es agradable.

Incluso antes de que la alineación de 16 bytes fuera oficialmente parte de la ABI, GCC solía habilitar -mpreferred-stack-boundary=4 (2 ^ 4 = 16-bytes) en 32 bits. Esto actualmente asume que la alineación de la stack entrante es de 16 bytes (incluso para los casos en los que fallará si no es así), así como preservar esa alineación. No estoy seguro de si las versiones históricas de gcc solían tratar de preservar la alineación de la stack sin depender de la corrección de los objetos SSE code-gen o alignas(16) .

ffmpeg es un ejemplo bien conocido que depende del comstackdor para darle la alineación de stack: ¿qué es “alineación de stack”? , por ejemplo, en Windows de 32 bits.

El gcc moderno todavía emite código en la parte superior de la línea main para alinear la stack en 16 (incluso en Linux, donde ABI garantiza que el kernel inicia el proceso con una stack alineada), pero no en la parte superior de ninguna otra función. Puedes usar -mincoming-stack-boundary para decirle a gcc qué tan alineado debe asumir que la stack es al generar el código.

El antiguo gcc4.1 no parecía respetar __attribute__((aligned(16))) o 32 para el almacenamiento automático, es decir, no molesta alinear la stack extra en este ejemplo en Godbolt , por lo que el viejo gcc tiene una especie de pasado a cuadros cuando se trata de la alineación de la stack. Creo que el cambio de la alineación oficial de Linux ABI a 16 bytes ocurrió primero como un cambio de facto, no como un cambio bien planeado. No he encontrado nada oficial cuando ocurrió el cambio, pero en algún momento entre 2005 y 2010, creo, después de que x86-64 se hizo popular y la alineación de stack de 16 bytes del System V ABI x86-64 resultó útil.

Al principio, fue un cambio al código-gen de GCC usar más alineamiento que el ABI requerido (es decir, usar un ABI más estricto para el código comstackdo por gcc), pero luego se escribió en la versión del i386 System V ABI mantenida en https : //github.com/hjl-tools/x86-psABI/wiki/X86-psABI (que es oficial para Linux al menos).


@MichaelPetch y @ThomasJager informan que gcc4.5 puede haber sido la primera versión en tener -mpreferred-stack-boundary=4 para 32 bits y 64 bits. gcc4.1.2 y gcc4.4.7 en Godbolt parecen comportarse de esa manera, por lo que tal vez el cambio se transfirió de nuevo, o Matt Godbolt configuró el viejo gcc con una configuración más moderna.