floating point - programacion - ¿Cuál es la velocidad relativa de punto flotante sumar contra punto flotante multiplicarse
punto flotante normalizado (6)
Hace una o dos décadas, valía la pena escribir un código numérico para evitar el uso de multiplicaciones y divisiones y, en su lugar, utilizar la suma y la resta. Un buen ejemplo es usar las diferencias hacia adelante para evaluar una curva polinomial en lugar de calcular el polinomio directamente.
¿Sigue siendo este el caso, o las arquitecturas de computadoras modernas han avanzado hasta el punto en que *, / ya no son muchas veces más lentas que +, -?
Para ser específico, estoy interesado en el código C / C ++ compilado que se ejecuta en los chips x86 típicos modernos con un extenso hardware de punto flotante incorporado, no un pequeño micro que intenta hacer FP en el software. Me doy cuenta de que la canalización y otras mejoras arquitectónicas impiden los recuentos de ciclos específicos, pero todavía me gustaría tener una intuición útil.
En teoría la información está aquí:
Para cada procesador que enumeran, la latencia en FMUL es muy similar a la de FADD o FDIV. En algunos de los procesadores más antiguos, FDIV es 2-3 veces más lento que eso, mientras que en los procesadores más nuevos, es lo mismo que FMUL.
Advertencias:
El documento que vinculé en realidad dice que no puede confiar en estos números en la vida real, ya que el procesador hará lo que quiera para hacer las cosas más rápido si es correcto.
Hay una buena posibilidad de que su compilador decida usar uno de los muchos conjuntos de instrucciones más nuevos que tienen una multiplicación / división de punto flotante disponible.
Este es un documento complicado que solo debe ser leído por los compiladores del compilador y podría haberlo entendido mal. Como no tengo claro por qué falta el número de latencia FDIV para algunas de las CPU.
La diferencia de velocidad de * / vs + - depende de la arquitectura de su procesador. En general y con x86 en particular, la diferencia de velocidad se ha reducido con los procesadores modernos. * debe estar cerca de +, en caso de duda: simplemente experimente. Si tiene un problema realmente difícil con muchas operaciones de FP, considere usar su GPU (GeForce, ...) que funciona como un procesador vectorial.
La mejor manera de responder a esta pregunta es escribir un punto de referencia / perfil del procesamiento que necesita hacer. Lo empírico debe usarse sobre lo teórico siempre que sea posible. Especialmente cuando es fácil de alcanzar.
Si ya conoce las diferentes implementaciones de Math que debe hacer, puede escribir algunas transfermations de código diferentes de las matemáticas y ver dónde aumenta su rendimiento. Esto permitirá que el procesador / compilador genere diferentes flujos de ejecución para llenar las tuberías del procesador y darle una respuesta concreta a su respuesta.
Si está interesado específicamente en el rendimiento de las instrucciones de tipo DIV / MUL / ADD / SUB, incluso podría lanzar algún ensamblaje en línea para controlar específicamente qué variantes de estas instrucciones se ejecutan. Sin embargo, debe asegurarse de mantener ocupadas las unidades de ejecución de varias unidades para tener una buena idea del rendimiento que el sistema puede ofrecer.
También hacer algo como esto le permitiría comparar el rendimiento en múltiples variaciones del procesador simplemente ejecutando el mismo programa en ellos, y también podría permitirle tener en cuenta las diferencias de la placa base.
Editar:
La arquitectura básica de a + - es idéntica. Así que lógicamente toman el mismo tiempo para calcular. * por otro lado, requiere capas múltiples, típicamente construidas con "sumadores completos" para completar una sola operación. Esto garantiza que si bien se puede emitir un * a la tubería en cada ciclo, tendrá una latencia más alta que un circuito de suma / resta. Una fp / operación se implementa normalmente utilizando un método de aproximación que converge iterativamente hacia la respuesta correcta en el tiempo. Estos tipos de aproximaciones se implementan típicamente a través de la multiplicación. Por lo tanto, para el punto flotante, generalmente se puede asumir que la división tomará más tiempo porque no es práctico "desenrollar" las multiplicaciones (que ya son un gran circuito en sí mismo) en la tubería de una multitud de circuitos multiplicadores. Aún así, el rendimiento de un sistema dado se mide mejor a través de pruebas.
No puedo encontrar una referencia definitiva, pero la experimentación extensa me dice que la multiplicación de flotadores en la actualidad es casi la misma velocidad que la suma y la resta, mientras que la división no lo es (pero tampoco "muchas veces" más lenta). Puede obtener la intuición que desea solo ejecutando sus propios experimentos; recuerde generar los números aleatorios (millones de ellos) por adelantado, léalos antes de comenzar el cronometraje y use los contadores de rendimiento de la CPU (sin ningún otro proceso en ejecución, como tanto como puede detenerlos desde) para una medición precisa!
Probablemente hay muy poca diferencia en el tiempo entre la multiplicación y la suma. Por otro lado, la división sigue siendo significativamente más lenta que la multiplicación debido a su naturaleza recursiva. en la arquitectura x86 moderna, se deben considerar las instrucciones sse cuando se realiza una operación de punto flotante en lugar de usar fpu. Aunque un buen compilador C / C ++ debería darle la opción de usar sse en lugar de fpu.
También depende de la mezcla de instrucciones. Su procesador tendrá varias unidades de cómputo en espera en cualquier momento, y obtendrá un rendimiento máximo si todas se llenan todo el tiempo. Por lo tanto, ejecutar un bucle de mul es tan rápido como ejecutar un bucle o agrega, pero no ocurre lo mismo si la expresión se vuelve más compleja.
Por ejemplo, toma este bucle:
for(int j=0;j<NUMITER;j++) {
for(int i=1;i<NUMEL;i++) {
bla += 2.1 + arr1[i] + arr2[i] + arr3[i] + arr4[i] ;
}
}
para NUMITER = 10 ^ 7, NUMEL = 10 ^ 2, ambas matrices se inicializaron a pequeños números positivos (NaN es mucho más lento), esto toma 6.0 segundos usando dobles en un proc de 64 bits. Si sustituyo el bucle con
bla += 2.1 * arr1[i] + arr2[i] + arr3[i] * arr4[i] ;
Solo toma 1.7 segundos ... así que ya que "superamos" las adiciones, los muls eran esencialmente libres; y la reducción de adiciones ayudó. Se pone más confuso:
bla += 2.1 + arr1[i] * arr2[i] + arr3[i] * arr4[i] ;
- la misma distribución mul / add, pero ahora la constante se agrega en lugar de multiplicarse - toma 3.7 segundos. Es probable que su procesador esté optimizado para realizar cálculos numéricos típicos de manera más eficiente; de modo que las sumas de muls y las sumas en escala de productos puntuales son tan buenas como se pueden obtener; Agregar constantes no es tan común, así que es más lento ...
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; /*someval == 2.1*/
De nuevo toma 1.7 segundos.
bla += someval + arr1[i] + arr2[i] + arr3[i] + arr4[i] ; /*someval == 2.1*/
(igual que el bucle inicial, pero sin adición constante costosa: 2.1 segundos)
bla += someval * arr1[i] * arr2[i] * arr3[i] * arr4[i] ; /*someval == 2.1*/
(en su mayoría muls, pero una adición: 1.9 segundos)
Así que básicamente; es difícil decir cuál es más rápido, pero si desea evitar cuellos de botella, lo más importante es tener una mezcla sana, evitar el NaN o el INF, evitar agregar constantes. Hagas lo que hagas, asegúrate de probar y probar varias configuraciones del compilador, ya que a menudo pequeños cambios pueden marcar la diferencia.
Algunos casos más:
bla *= someval; // someval very near 1.0; takes 2.1 seconds
bla *= arr1[i] ;// arr1[i] all very near 1.0; takes 66(!) seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; // 1.6 seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, 2.2 seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, floats 2.2 seconds
bla += someval * arr1[i]* arr2[i];// 0.9 in x64, 1.6 in x86
bla += someval * arr1[i];// 0.55 in x64, 0.8 in x86
bla += arr1[i] * arr2[i];// 0.8 in x64, 0.8 in x86, 0.95 in CLR+x64, 0.8 in CLR+x86