¿Por qué la ABI x86-64/AMD64 System V exige una alineación de pila 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 esta alineación de 16 bytes mejora el rendimiento. O, en todo caso, cuáles fueron los motivos por los que se eligió así.
editar : Creo que escribí la pregunta de forma engañosa. No estaba preguntando por qué el procesador hace las cosas más rápido con una memoria alineada de 16 bytes; esto se explica en todas partes de los documentos. En cambio, lo que quería saber es cómo la alineación forzada de 16 bytes es mejor que simplemente dejar que los programadores alineen la pila ellos mismos cuando sea necesario. Pregunto esto porque, según mi experiencia con el ensamblador, la aplicación de la pila tiene dos problemas: solo es útil en menos del 1% del código que se ejecuta (por lo que el otro 99% es en realidad una sobrecarga); y también es una fuente muy común de errores. Así que me pregunto cómo resulta realmente rentable al final. Si bien todavía tengo dudas sobre esto, acepto la respuesta de Peter, ya que contiene la respuesta más detallada a mi pregunta original.
TL:DR: permite una asignación de pila eficiente de variables con alignof(T) == 16
, como long double
y __m128i
, y de matrices locales para hacer que la vectorización SSE2 sea eficiente.
Para obtener detalles sobre cómo escribir un asm que respete la ABI, consulte glibc scanf Fallos de segmentación cuando se llama desde una función que no alinea RSP . ( scanf
es solo un ejemplo de una función en la que el asm generado por el compilador en la biblioteca se basa en esa garantía ABI, y se utiliza movaps
para copiar 16 bytes a la vez hacia y/o desde locales en la pila).
Tenga en cuenta que la versión actual de i386 System V ABI utilizada en Linux también requiere una alineación de pila de 16 bytes 1 . Consulte https://sourceforge.net/p/fbc/bugs/659/ para conocer un poco de historia y mi comentario en https://gcc.gnu.org/bugzilla/show_bug.cgi?id=40838#c91 para un intento de resumiendo la desafortunada historia de cómo i386 GNU/Linux + GCC se metió accidentalmente en una situación en la que un cambio incompatible con versiones anteriores de la ABI de i386 System V fue el menor de dos males.
Windows x64 también requiere una alineación de pila de 16 bytes antes de un call
, presumiblemente por motivaciones similares a las del x86-64 System V.
Además, semi-relacionado: x86-64 System V requiere que las matrices globales de 16 bytes y más se alineen en 16. Lo mismo para las matrices locales de >= 16 bytes o tamaño variable, aunque ese detalle solo es relevante en todas las funciones si sabes que se le pasa la dirección del inicio de una matriz, no un puntero en el medio. ( Diferente alineación de memoria para diferentes tamaños de búfer ). No le permite hacer suposiciones adicionales sobre un arbitrario int *
.
SSE2 es la base para x86-64__m128
, y creo que hacer que la ABI sea eficiente para tipos como y para la vectorización automática del compilador fue uno de los objetivos del diseño. La ABI tiene que definir cómo se pasan dichos argumentos como argumentos de función o por referencia.
La alineación de 16 bytes a veces es útil para variables locales en la pila (especialmente matrices), y garantizar la alineación de 16 bytes significa que los compiladores pueden obtenerla de forma gratuita siempre que sea útil, incluso si la fuente no la solicita explícitamente.
Si no se conocía la alineación de la pila relativa a un límite de 16 bytes, cada función que quisiera un local alineado necesitaría un and rsp, -16
e instrucciones adicionales para guardar/restaurar rsp
después de un desplazamiento desconocido a rsp
(ya sea 0
o -8
), por ejemplo, usar up rbp
para un puntero de marco. .
Sin AVX, los operandos de origen de la memoria deben estar alineados en 16 bytes. por ejemplo, paddd xmm0, [rsp+rdi]
fallos si el operando de la memoria está desalineado. Entonces, si no se conoce la alineación, tendría que 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 sobre las que el compilador desea vectorizar automáticamente, simplemente puede optar por alinearlas en 16.
También tenga en cuenta que las primeras CPU x86 (antes de Nehalem/Bulldozer) tenían una movups
instrucción que era más lenta movaps
incluso cuando el puntero estaba alineado. (Es decir, las cargas/almacenamiento no alineados en datos alineados eran muy lentos, además de evitar que las cargas se plegaran en una instrucción ALU). (Consulte las guías de optimización, la guía de microarcos y las tablas de instrucciones de Agner Fog para obtener más información sobre todo lo anterior).
Estos factores son el motivo por el cual una garantía es más útil que simplemente mantener "normalmente" la pila alineada. Poder crear código que realmente falla en una pila desalineada permite más oportunidades de optimización.
Las matrices alineadas también aceleran las funciones vectorizadas memcpy
// strcmp
cualquier función que no pueda asumir alineación, sino que la verifica y puede saltar directamente a sus bucles de vectores completos.
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 al menos 16 bytes de longitud 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 el arreglo. En general, el compilador 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, en su mayoría, solo ayuda cuando las funciones que se vectorizan automáticamente se pueden insertar en línea, pero generalmente hay otras locales que el compilador puede incluir en los espacios para no desperdiciar espacio en la pila. Y no desperdicia instrucciones siempre que exista una alineación de pila conocida. (Obviamente, los diseñadores de ABI podrían haber omitido esto si hubieran decidido no requerir una alineación de pila de 16 bytes).
Derrame/recarga de__m128
Por supuesto, lo hace gratuito alignas(16) char buf[1024];
en otros casos en los que la fuente solicita una alineación de 16 bytes.
Y también hay __m128
// __m128d
locales __m128i
. Es posible que el compilador no pueda mantener todos los vectores locales en registros (por ejemplo, distribuidos en una llamada de función, o que no haya suficientes registros), por lo que necesita poder derramarlos/recargarlos con movaps
, o como un operando de fuente de memoria para instrucciones ALU. por razones de eficiencia discutidas anteriormente.
Las cargas/almacenamiento que en realidad se dividen a lo largo de un límite de línea de caché (64 bytes) tienen penalizaciones de latencia significativas y también penalizaciones menores de rendimiento en las CPU modernas. La carga necesita datos de 2 líneas de caché separadas, por lo que se necesitan dos accesos al caché. (Y potencialmente 2 errores de caché, pero eso es raro en la memoria de pila).
Creo que movups
ya tenía ese costo incorporado para los vectores en CPU más antiguas, donde es costoso, pero aún así apesta. Abarcar un límite de página de 4k es mucho peor (en CPU anteriores a Skylake), ya que una carga o almacenamiento requiere ~100 ciclos si toca bytes en ambos lados de un límite de 4k. (También necesita 2 comprobaciones de TLB). La alineación natural hace imposibles las divisiones a través de cualquier límite más amplio , por lo que la alineación de 16 bytes fue suficiente para todo lo que puede hacer con SSE2.
max_align_t
tiene alineación de 16 bytes en la ABI x86-64 System V, debido a long double
(x87 de 10 bytes/80 bits). Se define como rellenado a 16 bytes por alguna extraña razón, a diferencia del código de 32 bits donde sizeof(long double) == 10
. La carga/almacenamiento de 10 bytes x87 es bastante lento de todos modos (como 1/3 del rendimiento de carga de double
o float
en Core2, 1/6 en P4 o 1/8 en K8), pero tal vez las penalizaciones por línea de caché y división de página fueron tan malas en CPU más antiguas que decidieron definirlo de esa manera. Creo que en las CPU modernas (tal vez incluso Core2), recorrer una matriz long double
no sería más lento con 10 bytes empaquetados, porque fld m80
sería un cuello de botella mayor que una línea de caché dividida cada ~ 6,4 elementos.
En realidad, el ABI se definió antes de que el silicio estuviera disponible para realizar pruebas comparativas ( allá por ~ 2000 ), pero esos números de K8 son los mismos que los de K7 (el modo de 32 bits/64 bits es irrelevante aquí). Crear long double
16 bytes permite 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
/ orps
.)
Relacionado: esta max_align_t
definición significa que malloc
siempre devuelve memoria alineada de 16 bytes en código x86-64. Esto le permite usarlo para cargas alineadas con SSE como _mm_load_ps
, pero dicho código puede fallar cuando se compila para 32 bits, donde alignof(max_align_t)
solo hay 8. (Úselo aligned_alloc
o lo que sea).
Otros factores ABI incluyen pasar __m128
valores en la pila (después de que xmm0-7 tenga los primeros 8 argumentos flotantes/vectoriales). Tiene sentido requerir una alineación de 16 bytes para los vectores en la memoria, de modo que la persona que llama pueda usarlos de manera eficiente y almacenarlos de manera eficiente. Mantener la alineación de la pila de 16 bytes en todo momento facilita que las funciones que necesitan alinear parte del espacio de paso de argumentos en 16.
Hay tipos como __m128
los que ABI garantiza que tienen alineación de 16 bytes . Si define un local, toma su dirección y pasa ese puntero a alguna otra función, ese local debe estar suficientemente alineado. Por lo tanto, mantener la alineación de la pila de 16 bytes va de la mano con proporcionar a algunos tipos una alineación de 16 bytes, lo que obviamente es una buena idea.
Hoy en día, es bueno atomic<struct_of_16_bytes>
poder obtener una alineación de 16 bytes de forma económica, por lo que lock cmpxchg16b
nunca cruza el límite de una línea de caché. Para el caso realmente raro en el que tienes un local atómico con almacenamiento automático y le pasas punteros a varios subprocesos...
Nota a pie de página 1: Linux de 32 bits
No todas las plataformas de 32 bits rompieron la compatibilidad con los binarios existentes y los archivos escritos a mano como lo hizo Linux; algunos como i386 NetBSD todavía solo usan el requisito histórico de alineación de pila de 4 bytes de la versión original de i386 SysV ABI.
La alineación histórica de la pila de 4 bytes también fue insuficiente para 8 bytes eficientes double
en las CPU modernas. fld
Los / no alineados fstp
son generalmente eficientes, excepto cuando cruzan el límite de una línea de caché (como otras cargas/almacenamiento), por lo que no es horrible, pero los alineados naturalmente son agradables.
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. Actualmente, esto supone que la alineación de la pila entrante es de 16 bytes (incluso en los casos en los que fallará si no lo es), además de preservar esa alineación. No estoy seguro de si las versiones históricas de gcc solían intentar preservar la alineación de la pila sin depender de ella para la corrección de la generación de código u alignas(16)
objetos SSE.
ffmpeg es un ejemplo bien conocido que depende del compilador para alinear la pila: ¿ qué es "alineación de la pila"? , por ejemplo, en Windows de 32 bits.
El gcc moderno todavía emite código en la parte superior main
para alinear la pila en 16 (incluso en Linux donde la ABI garantiza que el kernel inicia el proceso con una pila alineada), pero no en la parte superior de ninguna otra función. Podría utilizar -mincoming-stack-boundary
para indicarle a gcc qué tan alineada debe asumir que está la pila al generar código.
El antiguo gcc4.1 no parecía respetar realmente __attribute__((aligned(16)))
el 32
almacenamiento automático, es decir, no se molesta en alinear la pila más en este ejemplo en Godbolt , por lo que el antiguo gcc tiene un pasado accidentado en lo que respecta a la alineación de la pila. Creo que el cambio de la ABI oficial de Linux a una alineación de 16 bytes se produjo primero como un cambio de facto, no como un cambio bien planificado. No he encontrado nada oficial sobre cuándo ocurrió el cambio, pero creo que en algún momento entre 2005 y 2010, después de que x86-64 se hiciera popular y la alineación de pila de 16 bytes del System V ABI x86-64 resultó útil.
Al principio fue un cambio en la generación de código de GCC para usar más alineación de la que requería la ABI (es decir, usar una ABI más estricta para el código compilado por gcc), pero luego se escribió en la versión de la ABI i386 System V mantenida en https ://github.com/hjl-tools/x86-psABI/wiki/X86-psABI (que es oficial al menos para Linux).
@MichaelPetch y @ThomasJager informan que gcc4.5 puede haber sido la primera versión disponible -mpreferred-stack-boundary=4
tanto para 32 bits como para 64 bits. gcc4.1.2 y gcc4.4.7 en Godbolt parecen comportarse de esa manera, por lo que tal vez el cambio fue respaldado o Matt Godbolt configuró el antiguo gcc con una configuración más moderna.