Cómo usar las instrucciones Fused Multiply-Add (FMA) con SSE/AVX

7 minutos de lectura

He aprendido que algunas CPU Intel/AMD pueden multiplicar y sumar simultáneamente con SSE/AVX:
FLOPS por ciclo para Sandy Bridge y Haswell SSE2/AVX/AVX2.

Me gusta saber cómo hacer esto mejor en el código y también quiero saber cómo se hace internamente en la CPU. Me refiero a la arquitectura superescalar. Digamos que quiero hacer una suma larga como la siguiente en SSE:

//sum = a1*b1 + a2*b2 + a3*b3 +... where a is a scalar and b is a SIMD vector (e.g. from matrix multiplication)
sum = _mm_set1_ps(0.0f);
a1  = _mm_set1_ps(a[0]); 
b1  = _mm_load_ps(&b[0]);
sum = _mm_add_ps(sum, _mm_mul_ps(a1, b1));

a2  = _mm_set1_ps(a[1]); 
b2  = _mm_load_ps(&b[4]);
sum = _mm_add_ps(sum, _mm_mul_ps(a2, b2));

a3  = _mm_set1_ps(a[2]); 
b3  = _mm_load_ps(&b[8]);
sum = _mm_add_ps(sum, _mm_mul_ps(a3, b3));
...

Mi pregunta es ¿cómo se convierte esto en multiplicación y suma simultáneas? ¿Pueden los datos ser dependientes? Quiero decir, ¿puede la CPU hacer _mm_add_ps(sum, _mm_mul_ps(a1, b1)) simultáneamente o los registros utilizados en la multiplicación y suma tienen que ser independientes?

Por último, ¿cómo se aplica esto a FMA (con Haswell)? Es _mm_add_ps(sum, _mm_mul_ps(a1, b1)) convertido automáticamente a una sola instrucción FMA o micro-operación?

avatar de usuario
místico

El compilador puede fusionar sumas y multiplicaciones separadas, aunque esto cambie el resultado final (haciéndolo más preciso).

Un FMA tiene solo un redondeo (mantiene efectivamente una precisión infinita para el resultado de la multiplicación temporal interna), mientras que un ADD + MUL tiene dos.

Los estándares IEEE y C permiten esto cuando #pragma STDC FP_CONTRACT ON está en vigor, y los compiladores pueden tenerlo ON por defecto (pero no todos lo hacen). Gcc contrata a FMA de forma predeterminada (con el valor predeterminado -std=gnu*pero no -std=c*p.ej -std=c++14). para sonarsolo está habilitado con -ffp-contract=fast. (Con solo el #pragma habilitado, solo dentro de una sola expresión como a+b*cno en sentencias de C++ separadas).

Esto es diferente del punto flotante estricto frente al relajado (o en términos de gcc, -ffast-math contra -fno-fast-math) que permitiría otro tipo de optimizaciones que podrían incrementar el error de redondeo dependiendo de los valores de entrada. Este es especial por la infinita precisión del temporal interno FMA; si hubiera algún redondeo en el temporal interno, esto no estaría permitido en FP estricto.

Incluso si habilita el punto flotante relajado, el compilador aún puede optar por no fusionarse, ya que podría esperar que sepa lo que está haciendo si ya está usando intrínsecos.


Entonces la mejor manera para asegurarse de que realmente obtiene las instrucciones de FMA que desea, en realidad usa los intrínsecos provistos para ellas:

Intrínsecos de FMA3: (AVX2-Intel Haswell)

  • _mm_fmadd_pd()_mm256_fmadd_pd()
  • _mm_fmadd_ps(), _mm256_fmadd_ps()
  • y alrededor de un millón de otras variaciones…

Intrínsecos de FMA4: (XOP – Excavadora AMD)

  • _mm_macc_pd(), _mm256_macc_pd()
  • _mm_macc_ps(), _mm256_macc_ps()
  • y alrededor de un millón de otras variaciones…

  • Gracias, eso responde más o menos a mi pregunta sobre FMA. Realmente debería pasar algún tiempo aprendiendo algo de ensamblaje x86. Eso probablemente respondería a la mayoría de mis preguntas.

    usuario2088790

    10/04/2013 a las 18:39

  • En cuanto a su pregunta sobre si se puede hacer una multiplicación y una suma simultáneamente (FMA). La respuesta es no ya que la suma usa el resultado de la multiplicación. Entonces te comes la latencia de sumar + multiplicar. Una instrucción FMA realiza ambas instrucciones juntas, generalmente con la misma latencia que una sola instrucción múltiple. Así que el complemento es gratis.

    – Místico

    10/04/2013 a las 18:45


  • Gracias, eso es lo que pensé. Ahora solo necesito descubrir cómo organizar mi código para que la suma como la que definí anteriormente haga sumas independientes y se multiplique simultáneamente (para evitar latencias).

    usuario2088790

    10/04/2013 a las 19:10

  • Solo necesita separarlos tanto como sea necesario para alcanzar el rendimiento máximo. La ruta crítica está en las adiciones. La latencia de un addps son 3 ciclos. Pero el rendimiento es 1. Por lo tanto, necesita un mínimo de 3 cadenas de suma separadas para utilizarlo por completo. Actualmente tiene 4, por lo que es suficiente.

    – Místico

    10/04/2013 a las 19:41


  • Creo que su respuesta es engañosa ya que un compilador puede usar FMA de forma predeterminada sin romper las reglas de IEEE stackoverflow.com/a/34817983/2542702

    – bosón Z

    19 de enero de 2016 a las 12:55

avatar de usuario
bosón z

Probé el siguiente código en GCC 5.3, Clang 3.7, ICC 13.0.1 y MSVC 2015 (compilador versión 19.00).

float mul_add(float a, float b, float c) {
    return a*b + c;
}

__m256 mul_addv(__m256 a, __m256 b, __m256 c) {
    return _mm256_add_ps(_mm256_mul_ps(a, b), c);
}

Con las opciones de compilador correctas (ver más abajo), cada compilador generará un vfmadd instrucción (por ejemplo vfmadd213ss) desde mul_add. Sin embargo, solo MSVC no logra contratar mul_addv a un solo vfmadd instrucción (por ejemplo vfmadd213ps).

Las siguientes opciones del compilador son suficientes para generar vfmadd instrucciones (excepto con mul_addv con MSVC).

GCC:   -O2 -mavx2 -mfma
Clang: -O1 -mavx2 -mfma -ffp-contract=fast
ICC:   -O1 -march=core-avx2
MSVC:  /O1 /arch:AVX2 /fp:fast

GCC 4.9 no se contraerá mul_addv a una sola instrucción fma pero desde al menos GCC 5.1 lo hace. No sé cuándo los otros compiladores comenzaron a hacer esto.

  • Ver también #pragma STDC FP_CONTRACT ON. Stephen Canon señala que permite la contracción solo dentro de una sola declaración, no entre declaraciones. (Lists.llvm.org/pipermail/cfe-dev/2015-September/045110.html). También tenga en cuenta que gcc permite la contracción solo con -std=gnu*no con -std=c11 o lo que sea. (Y luego permite la contracción entre declaraciones, más allá de lo que IEEE + ISO C permiten estrictamente). Podría valer la pena probar otra función de prueba que usa variables separadas.

    – Peter Cordes

    08/09/2017 a las 19:37

  • @PeterCordes, vea este stackoverflow.com/q/34436233/2542702 y la respuesta de Stephen Canon. Creo que lo que está haciendo GCC está bien según la respuesta de Stephen (asumiendo que GCC no ignoró STDC FP_CONTRACT que lamentablemente es la última vez que revisé).

    – bosón Z

    11 de septiembre de 2017 a las 11:17

  • Tu pregunta allí solo se refiere a return a*b + c;no sobre float mul = a*b; return mul + c;. Lea atentamente la publicación de la lista de correo de Stephen: menciona que clang STDC FP_CONTRACT ON solo permite la contracción dentro de una expresión, a diferencia de los sonidos metálicos -ffp-contract=fast lo que también lo habilitaría para mi segundo ejemplo en este comentario. Es por eso que clang tiene separado on contra fast configuración para la opción de línea de comandos. Vea mis ediciones recientes a la respuesta de Mysticial sobre esta pregunta. Es más complicado de lo que pensé al principio 🙁

    – Peter Cordes

    11 de septiembre de 2017 a las 17:19


  • @PeterCordes, uno de mis puntos es que GCC ignora #pragma STDC FP_CONTRACT. Al menos la última vez que lo comprobé. Debería verificar esto nuevamente (por ejemplo, gnuc99 y c99 o lo que sea).

    – bosón Z

    13 de septiembre de 2017 a las 10:51

  • Creo que eso sigue siendo cierto. Y su comportamiento real va más allá de lo que #pragma STDC FP_CONTRACT ON lo permite, por lo que no es como ponerlo en ON de forma predeterminada y no proporcionar una forma de apagarlo. Creo por lo que he leído que IEEE + C no especifica un #pragma STDC FP_CONTRACT FASTaunque eso es un útil ajuste.

    – Peter Cordes

    13 de septiembre de 2017 a las 15:28

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad