c++ - Uso de las instrucciones de la CPU AVX: bajo rendimiento sin "/ arch: AVX"
performance visual-studio-2010 (2)
Mi código C ++ usa SSE y ahora quiero mejorarlo para que sea compatible con AVX cuando esté disponible. Entonces detecto cuando AVX está disponible y llamo a una función que usa comandos AVX. Uso Win7 SP1 + VS2010 SP1 y una CPU con AVX.
Para usar AVX, es necesario incluir esto:
#include "immintrin.h"
y luego puede usar funciones AVX intrínsecas como _mm256_mul_ps
, _mm256_mul_ps
, etc. El problema es que, de manera predeterminada, VS2010 produce código que funciona muy lentamente y muestra la advertencia:
advertencia C4752: se encontraron las Extensiones de Vector Avanzadas de Intel (R); considere usar / arch: AVX
Parece que VS2010 en realidad no usa las instrucciones AVX, sino que las emula. Agregué /arch:AVX
a las opciones del compilador y obtuve buenos resultados. Pero esta opción le dice al compilador que use comandos AVX en todas partes cuando sea posible. ¡Entonces mi código puede bloquearse en la CPU que no es compatible con AVX!
Entonces, la pregunta es cómo hacer que el compilador VS2010 produzca código AVX, pero solo cuando especifique los intrínsecos AVX directamente. Para SSE funciona, solo uso las funciones intrínsecas de SSE y produce código SSE sin ninguna opción de compilación como /arch:SSE
. Pero para AVX no funciona por alguna razón.
El comportamiento que está viendo es el resultado de un costoso cambio de estado.
Consulte la página 102 del manual de Agner Fog:
http://www.agner.org/optimize/microarchitecture.pdf
Cada vez que cambie incorrectamente entre las instrucciones SSE y AVX, pagará una penalización de ciclo extremadamente alta (~ 70).
Cuando compila sin /arch:AVX
, VS2010 generará instrucciones de SSE, pero seguirá utilizando AVX siempre que tenga intrínsecos de AVX. Por lo tanto, obtendrás un código que tiene instrucciones SSE y AVX, que tendrán esas penalizaciones de cambio de estado. (VS2010 lo sabe, por lo que emite la advertencia que está viendo).
Por lo tanto, debe usar todas las SSE o todas las AVX. Especificando /arch:AVX
le dice al compilador que use todo AVX.
Parece que estás intentando crear varias rutas de código: una para SSE y otra para AVX. Para esto, sugiero que separe su código SSE y AVX en dos unidades de compilación diferentes. (uno compilado con /arch:AVX
y otro sin) A continuación, /arch:AVX
y /arch:AVX
un despachador para elegir en función del hardware en el que se ejecuta.
Si necesita mezclar SSE y AVX, asegúrese de usar _mm256_zeroupper()
o _mm256_zeroall()
apropiada para evitar las penalizaciones de cambio de estado.
tl; dr
Use _mm256_zeroupper();
o _mm256_zeroall();
alrededor de secciones de código usando AVX (antes o después dependiendo de los argumentos de la función). Utilice únicamente la opción /arch:AVX
para archivos de origen con AVX en lugar de para un proyecto completo para evitar romper la compatibilidad con las rutas de código SSE con codificación heredada.
Porque
Creo que la mejor explicación está en el artículo de Intel, "Evitar sanciones de transición AVX-SSE" ( PDF ). El resumen dice:
La transición entre las instrucciones Intel® AVX de 256 bits y las instrucciones heredadas de Intel® SSE dentro de un programa puede causar penalizaciones de rendimiento porque el hardware debe guardar y restaurar los 128 bits superiores de los registros YMM.
La separación de su código AVX y SSE en diferentes unidades de compilación puede NO ser útil si cambia entre el código de llamada de los archivos de objeto habilitados para SSE y AVX, porque la transición puede ocurrir cuando las instrucciones o el ensamblaje de AVX se mezclan con cualquiera de (de Intel papel):
- Instrucciones intrínsecas de 128 bits
- Ensamblado en línea SSE
- Código de punto flotante C / C ++ que se compila para Intel® SSE
- Llamadas a funciones o bibliotecas que incluyen cualquiera de las anteriores
Esto significa que incluso puede haber penalizaciones cuando se vincula con un código externo usando SSE.
Detalles
Hay 3 estados de procesador definidos por las instrucciones AVX, y uno de los estados es donde todos los registros YMM están divididos, lo que permite que la mitad inferior sea utilizada por las instrucciones SSE . El documento de Intel " Transiciones de estado de Intel® AVX: Migración del código SSE a AVX " proporciona un diagrama de estos estados:
Cuando se encuentra en el estado B (modo AVX-256), todos los bits de los registros YMM están en uso. Cuando se llama a una instrucción SSE, debe producirse una transición al estado C, y aquí es donde hay una penalización. La mitad superior de todos los registros de YMM debe guardarse en un búfer interno antes de que SSE pueda comenzar, incluso si resultan ser ceros. El costo de las transiciones está en el "orden de 50-80 ciclos de reloj en hardware Sandy Bridge". También hay una penalización que va de C -> A, como se muestra en la figura 2.
También puede encontrar detalles sobre la penalización de conmutación de estado que causa esta ralentización en la página 130, Sección 9.12, "Transiciones entre modos VEX y no VEX" en http://www.agner.org/optimize/microarchitecture.pdf (de la versión actualizada el 14-08-2014), mencionada en la respuesta de Mystical . Según su guía, cualquier transición hacia / desde este estado toma "alrededor de 70 ciclos de reloj en Sandy Bridge". Tal como lo indica el documento de Intel, esta es una pena de transición evitable.
Resolución
Para evitar las penalizaciones de transición, puede eliminar todo el código SSE heredado, ordenar al compilador que convierta todas las instrucciones SSE a su forma VEX codificada de instrucciones de 128 bits (si el compilador es capaz) o poner los registros YMM en un estado cero conocido antes transición entre el código AVX y SSE. Esencialmente, para mantener la ruta del código SSE por separado, debe poner a cero los 128 bits superiores de los 16 registros YMM (emitiendo una instrucción VZEROUPPER
) después de cualquier código que use instrucciones AVX . Poner a cero estos bits manualmente fuerza una transición al estado A, y evita la costosa penalización ya que los valores YMM no necesitan ser almacenados en un buffer interno por hardware. El intrínseco que realiza esta instrucción es _mm256_zeroupper(); . La descripción de este intrínseco es muy informativa:
Este intrínseco es útil para borrar los bits superiores de los registros YMM al hacer la transición entre las instrucciones Intel® Advanced Vector Extensions (Intel® AVX) y las instrucciones heredadas Intel® Supplemental SIMD Extensions (Intel® SSE). No hay penalización de transición si una aplicación borra los bits superiores de todos los registros YMM (establece en ''0'') a través de
VZEROUPPER
, la instrucción correspondiente para este intrínseco, antes de la transición entre las extensiones Intel® Advanced Vector Extension (Intel® AVX) e Intel heredado. ® Instrucciones complementarias SIMD Extensions (Intel® SSE).
En Visual Studio 2010+ (tal vez incluso más), obtienes esto intrínseco con immintrin.h.
Tenga en cuenta que poner a cero los bits con otros métodos no elimina la penalización, se VZEROUPPER
VZEROALL
instrucciones VZEROUPPER
o VZEROALL
.
Una solución automática implementada por el Compilador Intel es insertar un VZEROUPPER
al comienzo de cada función que contenga el código Intel AVX si ninguno de los argumentos es un registro YMM o __m256i
tipo de datos __m256
/ __m256d
/ __m256i
, y al final de las funciones si el valor devuelto no es un registro YMM o __m256i
tipo de datos __m256
/ __m256d
/ __m256i
.
En la naturaleza
Esta solución VZEROUPPER
es utilizada por FFTW para generar una biblioteca con soporte SSE y AVX. Ver simd-avx.h :
/* Use VZEROUPPER to avoid the penalty of switching from AVX to SSE.
See Intel Optimization Manual (April 2011, version 248966), Section
11.3 */
#define VLEAVE _mm256_zeroupper
Luego VLEAVE();
se llama al final de cada función usando intrínsecos para las instrucciones AVX.