Cálculo de punto flotante vs entero en hardware moderno

Estoy haciendo un trabajo crítico de rendimiento en C ++, y actualmente estamos usando cálculos enteros para problemas que son inherentemente de coma flotante porque “es más rápido”. Esto causa un montón de problemas molestos y agrega un montón de código molesto.

Ahora, recuerdo haber leído acerca de cómo los cálculos de punto flotante eran tan lentos aproximadamente en los 386 días, cuando creo (IIRC) que había un coprocesador opcional. ¿Pero seguramente hoy en día con CPUs exponencialmente más complejas y potentes no hay diferencia en “velocidad” si se realiza un cálculo de punto flotante o entero? ¿Especialmente porque el tiempo real de cálculo es pequeño en comparación con algo así como causar un locking de la tubería o recuperar algo de la memoria principal?

Sé que la respuesta correcta es comparar el hardware objective, ¿cuál sería una buena manera de probar esto? Escribí dos pequeños progtwigs de C ++ y comparé su tiempo de ejecución con “tiempo” en Linux, pero el tiempo de ejecución real es muy variable (no ayuda a correr en un servidor virtual). Sin pasar todo el día corriendo cientos de puntos de referencia, haciendo gráficos, etc. ¿hay algo que pueda hacer para obtener una prueba razonable de la velocidad relativa? ¿Alguna idea o pensamiento? ¿Estoy completamente equivocado?

Los progtwigs que utilicé de la siguiente manera, no son idénticos de ninguna manera:

#include  #include  #include  #include  int main( int argc, char** argv ) { int accum = 0; srand( time( NULL ) ); for( unsigned int i = 0; i < 100000000; ++i ) { accum += rand( ) % 365; } std::cout << accum << std::endl; return 0; } 

Progtwig 2:

 #include  #include  #include  #include  int main( int argc, char** argv ) { float accum = 0; srand( time( NULL ) ); for( unsigned int i = 0; i < 100000000; ++i ) { accum += (float)( rand( ) % 365 ); } std::cout << accum << std::endl; return 0; } 

¡Gracias por adelantado!

Editar: La plataforma que me importa es la x86 regular o la x86-64 que se ejecuta en equipos Linux y Windows de escritorio.

Editar 2 (pegado de un comentario a continuación): tenemos una extensa base de código actualmente. Realmente me he encontrado con la generalización de que “no debemos usar float ya que el cálculo de enteros es más rápido”, y estoy buscando un modo (si esto es cierto) de refutar esta suposición generalizada. Me doy cuenta de que sería imposible predecir el resultado exacto para nosotros, excepto hacer todo el trabajo y perfilarlo después.

De todos modos, gracias por todas sus excelentes respuestas y ayuda. Siéntase libre de agregar cualquier cosa :).

Por desgracia, solo puedo darte una respuesta “depende” …

Desde mi experiencia, hay muchas, muchas variables de rendimiento … especialmente entre matemáticas de números enteros y coma flotante. Varía considerablemente de un procesador a otro (incluso dentro de la misma familia, como x86) porque diferentes procesadores tienen diferentes longitudes de “tubería”. Además, algunas operaciones generalmente son muy simples (como la adición) y tienen una ruta acelerada a través del procesador, y otras (como la división) tardan mucho, mucho más.

La otra gran variable es donde residen los datos. Si solo tiene que agregar algunos valores, todos los datos pueden residir en la caché, donde pueden enviarse rápidamente a la CPU. Una operación de punto flotante muy, muy lento que ya tiene los datos en la memoria caché será muchas veces más rápido que una operación entera donde un entero necesita ser copiado de la memoria del sistema.

Supongo que está haciendo esta pregunta porque está trabajando en una aplicación crítica para el rendimiento. Si está desarrollando para la architecture x86, y necesita un rendimiento adicional, es posible que desee examinar el uso de las extensiones SSE. Esto puede acelerar en gran medida la aritmética de punto flotante de precisión simple, ya que la misma operación se puede realizar en múltiples datos a la vez, además hay un banco de registros * separado para las operaciones SSE. (Noté en tu segundo ejemplo que usaste “float” en lugar de “double”, lo que me hace pensar que estás usando matemática de precisión simple).

* Nota: El uso de las antiguas instrucciones de MMX en realidad ralentizaría los progtwigs, porque esas viejas instrucciones en realidad usaban los mismos registros que la FPU, lo que hace imposible usar tanto la FPU como MMX al mismo tiempo.

Por ejemplo (los números menores son más rápidos),

Intel Xeon X5550 de 64 bits a 2.67 GHz, gcc 4.1.2 -O3

 short add/sub: 1.005460 [0] short mul/div: 3.926543 [0] long add/sub: 0.000000 [0] long mul/div: 7.378581 [0] long long add/sub: 0.000000 [0] long long mul/div: 7.378593 [0] float add/sub: 0.993583 [0] float mul/div: 1.821565 [0] double add/sub: 0.993884 [0] double mul/div: 1.988664 [0] 

Procesador AMD Opteron ™ Dual Core de 32 bits 265 @ 1.81GHz, gcc 3.4.6 -O3

 short add/sub: 0.553863 [0] short mul/div: 12.509163 [0] long add/sub: 0.556912 [0] long mul/div: 12.748019 [0] long long add/sub: 5.298999 [0] long long mul/div: 20.461186 [0] float add/sub: 2.688253 [0] float mul/div: 4.683886 [0] double add/sub: 2.700834 [0] double mul/div: 4.646755 [0] 

Como señaló Dan , incluso una vez que se normaliza la frecuencia del reloj (que puede ser engañosa en los diseños de tuberías), los resultados variarán enormemente según la architecture de la CPU ( rendimiento ALU / FPU individual, así como el número real de ALU / FPU disponibles por núcleo en diseños superescalares que influye en la cantidad de operaciones independientes que se pueden ejecutar en paralelo ; este último factor no lo ejerce el código siguiente, ya que todas las operaciones siguientes dependen secuencialmente).

Punto de referencia de operación de FPU / ALU de un hombre pobre:

 #include  #ifdef _WIN32 #include  #else #include  #endif #include  

Es probable que haya una diferencia significativa en la velocidad del mundo real entre matemática de punto fijo y coma flotante, pero el rendimiento teórico del mejor caso de la ALU frente a la FPU es completamente irrelevante. En cambio, la cantidad de registros enteros y de punto flotante (registros reales, no nombres de registro) en su architecture que su cómputo no usa (p. Ej. Para control de bucle), la cantidad de elementos de cada tipo que encajan en una línea de caché , optimizaciones posibles considerando las diferentes semánticas para matemática de coma entero vs. coma flotante: estos efectos dominarán. Las dependencias de datos de su algoritmo juegan un papel importante aquí, de modo que ninguna comparación general predecirá la brecha de rendimiento en su problema.

Por ejemplo, la sum entera es conmutativa, por lo que si el comstackdor ve un bucle como el utilizado para un punto de referencia (suponiendo que la información aleatoria se preparó de antemano para que no oscurezca los resultados), puede desenrollar el bucle y calcular sums parciales con sin dependencias, luego agréguelas cuando termine el ciclo. Pero con punto flotante, el comstackdor tiene que hacer las operaciones en el mismo orden que usted solicitó (usted tiene puntos de secuencia allí para que el comstackdor tenga que garantizar el mismo resultado, lo cual no permite el reordenamiento) por lo que existe una fuerte dependencia de cada adición el resultado de la anterior.

También es probable que ajuste más operandos enteros en la memoria caché a la vez. Por lo tanto, la versión de punto fijo podría superar a la versión flotante en un orden de magnitud incluso en una máquina donde la FPU tiene un rendimiento teóricamente más alto.

La adición es mucho más rápida que el rand , por lo que su progtwig es (especialmente) inútil.

Necesita identificar puntos de acceso de rendimiento y modificar de forma incremental su progtwig. Parece que tienes problemas con tu entorno de desarrollo que primero tendrás que resolver. ¿Es imposible ejecutar su progtwig en su PC para un pequeño conjunto de problemas?

Generalmente, intentar trabajos de FP con aritmética de enteros es una receta lenta.

TIL Esto varía (mucho). Aquí hay algunos resultados usando el comstackdor gnu (por cierto, también lo comprobé comstackndo en máquinas, gnu g ++ 5.4 de xenial es muchísimo más rápido que 4.6.3 de linaro en precisión)

Intel i7 4700MQ xenial

 short add: 0.822491 short sub: 0.832757 short mul: 1.007533 short div: 3.459642 long add: 0.824088 long sub: 0.867495 long mul: 1.017164 long div: 5.662498 long long add: 0.873705 long long sub: 0.873177 long long mul: 1.019648 long long div: 5.657374 float add: 1.137084 float sub: 1.140690 float mul: 1.410767 float div: 2.093982 double add: 1.139156 double sub: 1.146221 double mul: 1.405541 double div: 2.093173 

Intel i3 2370M tiene resultados similares

 short add: 1.369983 short sub: 1.235122 short mul: 1.345993 short div: 4.198790 long add: 1.224552 long sub: 1.223314 long mul: 1.346309 long div: 7.275912 long long add: 1.235526 long long sub: 1.223865 long long mul: 1.346409 long long div: 7.271491 float add: 1.507352 float sub: 1.506573 float mul: 2.006751 float div: 2.762262 double add: 1.507561 double sub: 1.506817 double mul: 1.843164 double div: 2.877484 

Intel (R) Celeron (R) 2955U (Chromebook Acer C720 con xenial)

 short add: 1.999639 short sub: 1.919501 short mul: 2.292759 short div: 7.801453 long add: 1.987842 long sub: 1.933746 long mul: 2.292715 long div: 12.797286 long long add: 1.920429 long long sub: 1.987339 long long mul: 2.292952 long long div: 12.795385 float add: 2.580141 float sub: 2.579344 float mul: 3.152459 float div: 4.716983 double add: 2.579279 double sub: 2.579290 double mul: 3.152649 double div: 4.691226 

DigitalOcean 1GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2 (ejecutándose con confianza)

 short add: 1.094323 short sub: 1.095886 short mul: 1.356369 short div: 4.256722 long add: 1.111328 long sub: 1.079420 long mul: 1.356105 long div: 7.422517 long long add: 1.057854 long long sub: 1.099414 long long mul: 1.368913 long long div: 7.424180 float add: 1.516550 float sub: 1.544005 float mul: 1.879592 float div: 2.798318 double add: 1.534624 double sub: 1.533405 double mul: 1.866442 double div: 2.777649 

Procesador AMD Opteron ™ 4122 (preciso)

 short add: 3.396932 short sub: 3.530665 short mul: 3.524118 short div: 15.226630 long add: 3.522978 long sub: 3.439746 long mul: 5.051004 long div: 15.125845 long long add: 4.008773 long long sub: 4.138124 long long mul: 5.090263 long long div: 14.769520 float add: 6.357209 float sub: 6.393084 float mul: 6.303037 float div: 17.541792 double add: 6.415921 double sub: 6.342832 double mul: 6.321899 double div: 15.362536 

Utiliza el código de http://pastebin.com/Kx8WGUfg como benchmark-pc.c

 g++ -fpermissive -O3 -o benchmark-pc benchmark-pc.c 

He realizado varios pases, pero parece ser que los números generales son los mismos.

Una excepción notable parece ser ALU mul vs FPU mul. La sum y la resta parecen trivialmente diferentes.

Aquí está lo anterior en forma de gráfico (haga clic para obtener el tamaño completo, más bajo es más rápido y preferible):

Gráfico de datos anteriores

Actualización para acomodar a @ Peter Cordes

https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc

i7 4700MQ Linux Ubuntu Xenial de 64 bits (se aplican todos los parches al 2018-03-13)

  short add: 0.773049 short sub: 0.789793 short mul: 0.960152 short div: 3.273668 int add: 0.837695 int sub: 0.804066 int mul: 0.960840 int div: 3.281113 long add: 0.829946 long sub: 0.829168 long mul: 0.960717 long div: 5.363420 long long add: 0.828654 long long sub: 0.805897 long long mul: 0.964164 long long div: 5.359342 float add: 1.081649 float sub: 1.080351 float mul: 1.323401 float div: 1.984582 double add: 1.081079 double sub: 1.082572 double mul: 1.323857 double div: 1.968488 

Procesador AMD Opteron ™ 4122 (alojamiento compartido de DreamHost preciso)

  short add: 1.235603 short sub: 1.235017 short mul: 1.280661 short div: 5.535520 int add: 1.233110 int sub: 1.232561 int mul: 1.280593 int div: 5.350998 long add: 1.281022 long sub: 1.251045 long mul: 1.834241 long div: 5.350325 long long add: 1.279738 long long sub: 1.249189 long long mul: 1.841852 long long div: 5.351960 float add: 2.307852 float sub: 2.305122 float mul: 2.298346 float div: 4.833562 double add: 2.305454 double sub: 2.307195 double mul: 2.302797 double div: 5.485736 

Intel Xeon E5-2630L v2 @ 2.4GHz (Trusty 64-bit, Digital Ocean VPS)

  short add: 1.040745 short sub: 0.998255 short mul: 1.240751 short div: 3.900671 int add: 1.054430 int sub: 1.000328 int mul: 1.250496 int div: 3.904415 long add: 0.995786 long sub: 1.021743 long mul: 1.335557 long div: 7.693886 long long add: 1.139643 long long sub: 1.103039 long long mul: 1.409939 long long div: 7.652080 float add: 1.572640 float sub: 1.532714 float mul: 1.864489 float div: 2.825330 double add: 1.535827 double sub: 1.535055 double mul: 1.881584 double div: 2.777245 

Dos puntos a considerar –

El hardware moderno puede superponer instrucciones, ejecutarlas en paralelo y reordenarlas para hacer el mejor uso del hardware. Y también, cualquier progtwig de punto flotante significativo probablemente también tenga un entero entero significativo, incluso si solo calcula índices en matrices, contador de bucles, etc. por lo que incluso si tiene una instrucción de coma flotante lenta, puede estar ejecutándose en un hardware separado superpuesto con algunos del trabajo entero. Mi punto es que incluso si las instrucciones de coma flotante son lentas que las enteros, su progtwig general puede correr más rápido porque puede hacer uso de más hardware.

Como siempre, la única forma de estar seguro es perfilar su progtwig real.

El segundo punto es que la mayoría de las CPU actualmente tienen instrucciones SIMD para coma flotante que pueden operar en múltiples valores de coma flotante, todo al mismo tiempo. Por ejemplo, puede cargar 4 flotadores en un solo registro SSE y realizar 4 multiplicaciones en todos ellos en paralelo. Si puede reescribir partes de su código para usar instrucciones SSE, entonces es probable que sea más rápido que una versión entera. Visual c ++ proporciona funciones intrínsecas del comstackdor para hacerlo, consulte http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx para obtener más información.

Ejecuté una prueba que acaba de agregar 1 al número en lugar de rand (). Los resultados (en un x86-64) fueron:

  • corto: 4.260s
  • int: 4.020s
  • larga duración: 3.350s
  • flotador: 7.330s
  • doble: 7.210s

A menos que esté escribiendo código que se llamará millones de veces por segundo (como, por ejemplo, dibujar una línea en la pantalla en una aplicación de gráficos), la aritmética de números enteros frente a coma flotante raramente es el cuello de botella.

El primer paso habitual para las preguntas de eficiencia es perfilar su código para ver dónde se gasta realmente el tiempo de ejecución. El comando de Linux para esto es gprof .

Editar:

Aunque supongo que siempre puedes implementar el algoritmo de dibujo de líneas usando enteros y números de coma flotante, llámalo un gran número de veces y ve si hace una diferencia:

http://en.wikipedia.org/wiki/Bresenham's_algorithm

La versión de coma flotante será mucho más lenta, si no hay operación restante. Como todas las adiciones son secuenciales, la CPU no podrá paralelizar la sum. La latencia será crítica. La latencia de agregación de FPU suele ser de 3 ciclos, mientras que la sum entera es de 1 ciclo. Sin embargo, el divisor para el operador restante probablemente sea la parte crítica, ya que no está completamente canalizado en la CPU moderna. por lo tanto, suponiendo que la instrucción de dividir / restar consumirá la mayor parte del tiempo, la diferencia para agregar latencia será pequeña.

En la actualidad, las operaciones de enteros suelen ser un poco más rápidas que las operaciones de punto flotante. Entonces, si puede hacer un cálculo con las mismas operaciones en números enteros y coma flotante, use enteros. SIN EMBARGO, usted está diciendo “Esto causa un montón de problemas molestos y agrega un montón de código molesto”. Parece que necesita más operaciones porque usa aritmética de enteros en lugar de coma flotante. En ese caso, el punto flotante se ejecutará más rápido porque

  • tan pronto como necesite más operaciones enteras, es probable que necesite mucho más, por lo que la ventaja de la velocidad leve es más que comido por las operaciones adicionales

  • el código de punto flotante es más simple, lo que significa que es más rápido escribir el código, lo que significa que si es crítico para la velocidad, puede pasar más tiempo optimizando el código.

Basado en ese “algo que he escuchado” tan confiable, en los viejos tiempos, el cálculo de enteros era entre 20 y 50 veces más rápido que el punto flotante, y actualmente es menos del doble de rápido.