¿Por qué gcc no resuelve _mm256_loadu_pd como vmovupd único?
Estoy escribiendo código AVX y necesito cargarlo desde una memoria potencialmente no alineada. Actualmente estoy cargando 4 dobles , por lo tanto usaría la instrucción intrínseca _mm256_loadu_pd ; el código que he escrito es:
__m256d d1 = _mm256_loadu_pd(vInOut + i*4);
Luego compilé con opciones -O3 -mavx -g
y posteriormente utilicé objdump para obtener el código ensamblador más el código anotado y la línea ( objdump -S -M intel -l avx.obj
).
Cuando miro el código ensamblador subyacente, encuentro lo siguiente:
vmovupd xmm0,XMMWORD PTR [rsi+rax*1]
vinsertf128 ymm0,ymm0,XMMWORD PTR [rsi+rax*1+0x10],0x1
Esperaba ver esto:
vmovupd ymm0,XMMWORD PTR [rsi+rax*1]
y usar completamente el registro de 256 bits ( ymm0 ), en su lugar parece que gcc ha decidido completar la parte de 128 bits ( xmm0 ) y luego cargar nuevamente la otra mitad con vinsertf128 .
¿Alguien puede explicar esto?
El código equivalente se compila con un único vmovupd en MSVC VS 2012.
Estoy ejecutando gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0
Ubuntu 18.04 x86-64 .
El ajuste predeterminado de GCC ( -mtune=generic
) incluye -mavx256-split-unaligned-load
y-mavx256-split-unaligned-store
, porque proporciona una aceleración menor en algunas CPU (por ejemplo, Sandybridge de primera generación y algunas CPU AMD) en algunos casos cuando la memoria en realidad está desalineada en tiempo de ejecución.
Úsalo -O3 -mno-avx256-split-unaligned-load -mno-avx256-split-unaligned-store
si no quieres esto, o mejor, usa -mtune=haswell
. O utilícelo -march=native
para optimizar para su propia computadora. No hay ningún ajuste "generic-avx2". ( https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html ).
Intel Sandybridge ejecuta cargas de 256 bits como un único uop que requiere 2 ciclos en un puerto de carga. (A diferencia de AMD, que decodifica todas las instrucciones vectoriales de 256 bits como 2 uops separados). Sandybridge tiene un problema con cargas de 256 bits no alineadas (si la dirección realmente está desalineada en tiempo de ejecución). No conozco los detalles y no he encontrado mucha información específica sobre exactamente qué es la desaceleración. ¿Quizás porque utiliza un caché bancario, con bancos de 16 bytes? Pero IvyBridge maneja mejor las cargas de 256 bits y aún tiene caché almacenado.
Según el mensaje de la lista de correo de GCC sobre el código que implementa la opción ( https://gcc.gnu.org/ml/gcc-patches/2011-03/msg01847.html ), " Acelera algunas pruebas comparativas de SPEC CPU 2006 al hasta un 6% ". (Creo que eso es para Sandybridge, la única CPU Intel AVX que existía en ese momento).
Pero si la memoria en realidad está alineada con 32 bytes en tiempo de ejecución, esto es una verdadera desventaja incluso en Sandybridge y la mayoría de las CPU AMD 1 . Entonces, con esta opción de ajuste, es posible que pierda simplemente por no informarle a su compilador sobre las garantías de alineación. Y si su bucle se ejecuta en memoria alineada la mayor parte del tiempo, será mejor que compile al menos esa unidad de compilación con -mno-avx256-split-unaligned-load
opciones de ajuste que implican eso.
La división del software impone el costo todo el tiempo. Dejar que el hardware se encargue de ello hace que la carcasa alineada sea perfectamente eficiente (excepto las tiendas en Piledriver 1 ), y la carcasa desalineada posiblemente sea más lenta que con la división del software en algunas CPU. Por lo tanto, es un enfoque pesimista y tiene sentido si es realmente probable que los datos realmente estén desalineados en tiempo de ejecución, en lugar de simplemente no garantizar que estén siempre alineados en tiempo de compilación. por ejemplo, tal vez tenga una función que se llama la mayor parte del tiempo con buffers alineados, pero aún desea que funcione en casos raros/pequeños en los que se llama con buffers desalineados. En ese caso, una estrategia de división de carga/almacenamiento es inapropiada incluso en Sandybridge.
Es común que los buffers estén alineados con 16 bytes pero no con 32 bytes porque malloc
en x86-64 glibc (y new
en libstdc++) devuelve buffers alineados de 16 bytes (porque alignof(maxalign_t) == 16
). Para buffers grandes, el puntero normalmente está a 16 bytes después del inicio de una página, por lo que siempre está desalineado para alineaciones mayores a 16. Úselo aligned_alloc
en su lugar.
Tenga en cuenta esto -mavx
y -mavx2
no cambie las opciones de ajuste en absoluto : gcc -O3 -mavx2
aún ajusta todas las CPU, incluidas aquellas que en realidad no pueden ejecutar instrucciones AVX2. Esto es bastante tonto, porque deberías usar una única carga no alineada de 256 bits si ajustas "la CPU AVX2 promedio". Desafortunadamente, gcc no tiene opción para hacer eso y -mavx2
no implica -mno-avx256-split-unaligned-load
nada. Consulte https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80568 y https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78762 para solicitudes de funciones que tengan influencia en la selección del conjunto de instrucciones. Afinación .
Es por eso que debería -march=native
crear archivos binarios para uso local, o tal vez -march=sandybridge -mtune=haswell
crear archivos binarios que puedan ejecutarse en una amplia gama de máquinas, pero que probablemente se ejecuten principalmente en hardware más nuevo que tenga AVX. (Tenga en cuenta que incluso las CPU Skylake Pentium/Celeron no tienen AVX o BMI2; probablemente en las CPU con defectos en la mitad superior de las unidades de ejecución de 256 bits o archivos de registro, desactivan la decodificación de los prefijos VEX y los venden como de gama baja Pentium.)
Las opciones de ajuste de gcc8.2 son las siguientes. ( -march=x
implica -mtune=x
). https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html .
Revisé el explorador del compilador Godbolt compilando -O3 -fverbose-asm
y mirando los comentarios que incluyen un volcado completo de todas las opciones implícitas. Incluí _mm256_loadu/storeu_ps
funciones y un bucle flotante simple que puede vectorizarse automáticamente, para que también podamos ver lo que hace el compilador.
Utilice -mprefer-vector-width=256
(gcc8) o -mno-prefer-avx128
(gcc7 y versiones anteriores) para anular opciones de ajuste como -mtune=bdver3
y obtener vectorización automática de 256 bits si lo desea, en lugar de solo con vectorización manual.
- predeterminado/
-mtune=generic
: ambos-mavx256-split-unaligned-load
y-store
. Podría decirse que cada vez son menos apropiados como Intel Haswell y luego se vuelven más comunes, y creo que la desventaja de las CPU AMD recientes es todavía pequeña. Especialmente dividir cargas no alineadas , que las opciones de ajuste de AMD no habilitan. -march=sandybridge
y-march=ivybridge
: dividir ambos. (Creo que he leído que IvyBridge mejoró el manejo de cargas o almacenes de 256 bits no alineados, por lo que es menos apropiado para casos en los que los datos pueden estar alineados en tiempo de ejecución).-march=haswell
y posterior: ninguna opción de división habilitada.-march=knl
: ninguna opción de división habilitada. (Silvermont/Atom no tienen AVX)-mtune=intel
: ninguna opción de división habilitada. Incluso con gcc8, la vectorización automática-mtune=intel -mavx
elige alcanzar un límite de alineación para la matriz de destino de lectura/escritura, a diferencia de la estrategia normal de gcc8 de simplemente usar no alineado. (Nuevamente, otro caso de manejo de software que siempre tiene un costo versus dejar que el hardware se encargue del caso excepcional).
-march=bdver1
(Bulldozer):-mavx256-split-unaligned-store
, pero no cargas. También configura el gcc7 equivalente a gcc8 y versiones anteriores-mprefer-avx128
(la vectorización automática solo usará AVX de 128 bits, pero, por supuesto, los intrínsecos aún pueden usar vectores de 256 bits).-march=bdver2
(Piledriver),bdver3
(Apisonadora),bdver4
(Excavadora). Lo mismo que Bulldozer. ¡Vectorizan automáticamente una[i] += b[i]
bucle FP con captación previa de software y suficiente desenrollado para realizar una captación previa solo una vez por línea de caché!-march=znver1
(Zen):-mavx256-split-unaligned-store
pero no se carga, todavía se autovectoriza con solo 128 bits, pero esta vez sin captación previa de SW.-march=btver2
( AMD Fam16h, también conocido como Jaguar ): ninguna opción de división habilitada, vectorización automática como la familia Bulldozer con solo vectores de 128 bits + captación previa de SW.-march=eden-x4
(A través de Eden con AVX2): ninguna opción de división está habilitada, pero la-march
opción ni siquiera lo habilita-mavx
, y la vectorización automática usamovlps
/movhps
cargas de 8 bytes, lo cual es realmente tonto. Al menos utilícelomovsd
en lugar demovlps
para romper la falsa dependencia. Pero si habilita-mavx
, utiliza cargas no alineadas de 128 bits. Comportamiento realmente extraño/inconsistente aquí, a menos que haya alguna interfaz extraña para esto.opciones (habilitadas como parte de -march=sandybridge por ejemplo, presumiblemente también para Bulldozer-family (-march=bdver2 es piledriver). Sin embargo, eso no resuelve el problema cuando el compilador sabe que la memoria está alineada.
Nota al pie 1: AMD Piledriver tiene un error de rendimiento que hace que el rendimiento de la tienda de 256 bits sea terrible: incluso vmovaps [mem], ymm
las tiendas alineadas ejecutan una cada 17 a 20 relojes según el pdf del microarco de Agner Fog ( https://agner.org/optimize/ ). Este efecto no está presente en Bulldozer o Steamroller/Excavator.
Agner Fog dice que el rendimiento de AVX de 256 bits en general (no carga/almacena específicamente) en Bulldozer/Piledriver suele ser peor que AVX de 128 bits, en parte porque no puede decodificar instrucciones en un patrón de 2-2 uop. Steamroller hace que 256 bits esté cerca del punto de equilibrio (si no cuesta barajar más). Pero las instrucciones de registro-registro vmovaps ymm
todavía solo se benefician de la eliminación de movimientos para los 128 bits bajos en la familia Bulldozer.
Pero el software de código cerrado o las distribuciones binarias generalmente no pueden darse el lujo de construir en -march=native
cada arquitectura de destino, por lo que existe una compensación al crear un binario que pueda ejecutarse en cualquier CPU compatible con AVX. Por lo general, vale la pena obtener una gran aceleración con código de 256 bits en algunas CPU, siempre y cuando no haya desventajas catastróficas en otras CPU.
Dividir cargas/almacenamiento no alineados es un intento de evitar grandes problemas en algunas CPU. Cuesta un rendimiento de uop adicional y uops de ALU adicionales en las CPU más recientes. Pero al menos vinsertf128 ymm, [mem], 1
no necesita la unidad aleatoria en el puerto 5 de Haswell/Skylake: puede ejecutarse en cualquier puerto ALU vectorial. (Y no tiene microfusión, por lo que cuesta 2 uops de ancho de banda frontal).
PD:
La mayor parte del código no es compilado por compiladores de última generación, por lo que cambiar el ajuste "genérico" ahora tomará un tiempo antes de que el código compilado con un ajuste actualizado entre en uso. (Por supuesto, la mayor parte del código se compila solo con -O2
o -O3
, y esta opción solo afecta a la generación de código AVX de todos modos. Pero desafortunadamente muchas personas usan -O3 -mavx2
en lugar de -O3 -march=native
. Por lo tanto, pueden perderse FMA, BMI1/2, popcnt y otras cosas de su CPU. soportes.
El ajuste genérico de GCC divide cargas de 256 bits no alineadas para ayudar a los procesadores más antiguos. (Creo que los cambios posteriores evitan dividir las cargas en el ajuste genérico).
Puede sintonizar CPU Intel más recientes usando algo como -mtune=intel
o -mtune=skylake
y obtendrá una única instrucción, según lo previsto.