Estoy haciendo un trabajo crítico para el rendimiento en C ++ y actualmente estamos usando cálculos de números enteros para problemas que son inherentemente de punto flotante porque "es más rápido". Esto causa muchos problemas molestos y agrega mucho código molesto.
Ahora, recuerdo haber leído sobre cómo los cálculos de coma flotante eran tan lentos aproximadamente alrededor de los 386 días, donde 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 la "velocidad" si se hace un cálculo de punto flotante o entero. ¿Especialmente porque el tiempo de cálculo real es pequeño en comparación con algo como causar un bloqueo de la tubería o recuperar algo de la memoria principal?
Sé que la respuesta correcta es comparar el hardware de destino, ¿cuál sería una buena manera de probar esto? Escribí dos pequeños programas en C ++ y comparé su tiempo de ejecución con el "tiempo" en Linux, pero el tiempo de ejecución real es demasiado variable (no ayuda, estoy ejecutando en un servidor virtual). Aparte de pasar todo el día ejecutando 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 programas que utilicé de la siguiente manera, no son idénticos de ninguna manera:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
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;
}
Programa 2:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
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 x86 o x86-64 normal que se ejecuta en máquinas de escritorio Linux y Windows.
Edición 2 (pegado de un comentario a continuación): Actualmente tenemos una base de código extensa. Realmente me he encontrado con la generalización de que "no debemos usar float ya que el cálculo de números enteros es más rápido", y estoy buscando una manera (si es que esto es cierto) para refutar esta suposición generalizada. Me doy cuenta de que sería imposible predecir el resultado exacto para nosotros sin 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 otra cosa :).
fuente
addl
reemplazado porfadd
, por ejemplo). La única forma de obtener una buena medición es obtener una parte central de su programa real y perfilar diferentes versiones de eso. Desafortunadamente, eso puede ser bastante difícil sin usar toneladas de esfuerzo. Quizás decirnos el hardware de destino y su compilador ayudaría a la gente al menos a brindarle experiencia preexistente, etc. Sobre su uso de enteros, sospecho que podría crear una especie de clase defixed_point
plantilla que facilitaría enormemente ese trabajo.float
obtiene el aumento de velocidad, pero generalmentedouble
no lo hace.Respuestas:
Por desgracia, solo puedo darte una respuesta de "depende" ...
Según mi experiencia, hay muchas, muchas variables para el rendimiento ... especialmente entre las matemáticas de punto flotante y entero. Varía mucho de un procesador a otro (incluso dentro de la misma familia, como x86) porque diferentes procesadores tienen diferentes longitudes de "canalización". Además, algunas operaciones son generalmente muy simples (como la suma) y tienen una ruta acelerada a través del procesador, y otras (como la división) toman mucho, mucho más tiempo.
La otra gran variable es dónde residen los datos. Si solo tiene unos pocos valores para agregar, entonces todos los datos pueden residir en la caché, donde pueden enviarse rápidamente a la CPU. Una operación de punto flotante muy, muy lenta que ya tiene los datos en la caché será muchas veces más rápida que una operación de entero donde un entero necesita ser copiado de la memoria del sistema.
Supongo que está haciendo esta pregunta porque está trabajando en una aplicación de rendimiento crítico. Si está desarrollando para la arquitectura x86 y necesita un rendimiento adicional, es posible que desee considerar el uso de las extensiones SSE. Esto puede acelerar enormemente la aritmética de punto flotante de precisión simple, ya que la misma operación se puede realizar en varios datos a la vez, además de que hay un banco de registros separado * para las operaciones SSE. (Me di cuenta de que en su segundo ejemplo usó "flotar" en lugar de "doble", lo que me hace pensar que está usando matemáticas de precisión simple).
* Nota: El uso de las antiguas instrucciones MMX en realidad ralentizaría los programas, porque esas instrucciones antiguas en realidad usaban los mismos registros que la FPU, lo que imposibilita el uso de la FPU y MMX al mismo tiempo.
fuente
double
-precision FP. Con solo dos de 64 bitsdouble
por registro, la aceleración potencial es menor quefloat
para el código que se vectoriza bien. Escalarfloat
ydouble
usar registros XMM en x86-64, con x87 heredado solo usado paralong double
. (Entonces @ Dan: no, los registros MMX no entran en conflicto con los registros FPU normales, porque el FPU normal en x86-64 es la unidad SSE. MMX no tendría sentido porque si puede hacer SIMD entero, quiere 16 bytes enxmm0..15
lugar de 8 -bytemm0..7
, y las CPU modernas tienen peor rendimiento MMX que SSE.)Por ejemplo (números menores son más rápidos),
Intel Xeon X5550 de 64 bits a 2,67 GHz, gcc 4.1.2
-O3
Procesador AMD Opteron (tm) de doble núcleo de 32 bits 265 a 1,81 GHz, gcc 3,4.6
-O3
Como señaló Dan , incluso una vez que se normalice la frecuencia del reloj (que puede ser engañoso en sí mismo en diseños canalizados), los resultados variarán enormemente según la arquitectura de la CPU ( rendimiento individual de ALU / FPU , 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 a continuación, ya que todas las operaciones a continuación son secuencialmente dependientes).
Punto de referencia de la operación FPU / ALU del pobre hombre:
fuente
volatile
para asegurarme. En Win64, la FPU no se utiliza y MSVC no generará código para ello, por lo que compila utilizandomulss
ydivss
las instrucciones XMM allí, que son 25 veces más rápido que el FPU en Win32. La máquina de prueba es Core i5 M 520 @ 2.40GHzv
alcanzarán rápidamente 0 o +/- inf muy muy rápidamente, lo que puede o no ser (teóricamente) tratado como un caso especial / acelerado por ciertas implementaciones de fpu.v
). En los diseños recientes de Intel, la división no se canaliza en absoluto (divss
/divps
tiene una latencia de 10-14 ciclos y el mismo rendimiento recíproco).mulss
sin embargo, tiene una latencia de 5 ciclos, pero puede emitir uno en cada ciclo. (O dos por ciclo en Haswell, ya que el puerto 0 y el puerto 1 tienen un multiplicador para FMA).Es probable que haya una diferencia significativa en la velocidad del mundo real entre las matemáticas de punto fijo y de punto flotante, pero el rendimiento teórico en el mejor de los casos de ALU frente a FPU es completamente irrelevante. En cambio, el número de registros enteros y de punto flotante (registros reales, no nombres de registro) en su arquitectura que de otra manera no son utilizados por su cálculo (por ejemplo, para el control de bucle), el número de elementos de cada tipo que caben en una línea de caché , las optimizaciones son posibles considerando las diferentes semánticas para las matemáticas enteras frente a las de punto flotante: estos efectos dominarán. Las dependencias de datos de su algoritmo juegan un papel importante aquí, por lo que ninguna comparación general predecirá la brecha de rendimiento en su problema.
Por ejemplo, la suma de enteros es conmutativa, por lo que si el compilador ve un bucle como el que usó para un punto de referencia (asumiendo que los datos aleatorios se prepararon de antemano para que no oscurezcan los resultados), puede desenrollar el bucle y calcular sumas parciales con sin dependencias, luego agréguelas cuando termine el ciclo. Pero con el punto flotante, el compilador tiene que realizar las operaciones en el mismo orden que usted solicitó (tiene puntos de secuencia allí, por lo que el compilador debe garantizar el mismo resultado, lo que no permite el reordenamiento), por lo que existe una fuerte dependencia de cada adición en el resultado del anterior.
Es probable que también quepa más operandos enteros en la 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.
fuente
La adición es mucho más rápida que
rand
, por lo que su programa es (especialmente) inútil.Debe identificar los puntos críticos de rendimiento y modificar gradualmente su programa. Parece que tiene problemas con su entorno de desarrollo que deberán resolverse primero. ¿Es imposible ejecutar su programa en su PC para un pequeño conjunto de problemas?
Generalmente, intentar trabajos FP con aritmética de enteros es una receta para la lentitud.
fuente
timespec_t
o algo similar. Registre el tiempo al principio y al final del ciclo y tome la diferencia. Luego mueva larand
generación de datos fuera del ciclo. Asegúrese de que su algoritmo obtenga todos sus datos de matrices y coloque todos sus datos en matrices. Eso obtiene su algoritmo real por sí mismo y obtiene la configuración, malloc, impresión de resultados, todo menos el cambio de tareas y las interrupciones de su ciclo de creación de perfiles.TIL Esto varía (mucho). Aquí hay algunos resultados usando el compilador gnu (por cierto, también verifiqué compilando 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
Intel i3 2370M tiene resultados similares
Intel (R) Celeron (R) 2955U (Chromebook Acer C720 con xenial)
DigitalOcean CPU Intel (R) Xeon (R) Droplet de 1GB E5-2630L v2 (funcionamiento confiable)
Procesador AMD Opteron (tm) 4122 (preciso)
Esto usa código de http://pastebin.com/Kx8WGUfg como
benchmark-pc.c
He realizado varias pasadas, pero este parece ser el caso de que los números generales son los mismos.
Una excepción notable parece ser ALU mul vs FPU mul. La suma y la resta parecen trivialmente diferentes.
Aquí está lo anterior en forma de gráfico (haga clic para ver el tamaño completo, más bajo es más rápido y preferible):
Actualización para acomodar a @Peter Cordes
https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc
i7 4700MQ Linux Ubuntu Xenial de 64 bits (se aplicaron todos los parches para 2018-03-13) Procesador AMD Opteron (tm) 4122 (preciso, alojamiento compartido DreamHost) Intel Xeon E5-2630L v2 a 2,4 GHz (Trusty 64 bits, DigitalOcean VPS)fuente
benchmark-pc
midiendo alguna combinación de rendimiento y latencia? En su Haswell (i7 4700MQ), la multiplicación de números enteros es 1 por rendimiento de reloj, latencia de 3 ciclos, pero la suma / sustitución de enteros es 4 por rendimiento de reloj, latencia de 1 ciclo ( agner.org/optimize ). Entonces, presumiblemente, hay una gran cantidad de bucles que diluyen esos números para que add y mul salgan tan cerca (long add: 0.824088 vs. long mul: 1.017164). (gcc por defecto no desenrolla bucles, excepto para desenrollar completamente los recuentos de iteraciones muy bajos).int
, soloshort
ylong
? En Linux x86-64,short
es de 16 bits (y por lo tanto tiene ralentizaciones de registro parcial en algunos casos), mientras quelong
ylong long
son ambos tipos de 64 bits. (¿Quizás esté diseñado para Windows, donde x86-64 todavía usa 32 bitslong
? O quizás está diseñado para el modo de 32 bits). En Linux, el ABI x32 tiene 32 bitslong
en modo de 64 bits , así que si tiene las bibliotecas instaladas , se usagcc -mx32
para compilar para ILP32. O simplemente usa-m32
y mira loslong
números.addps
en registros xmm en lugar deaddss
hacer 4 adiciones FP en paralelo en una instrucción que es tan rápida como escalaraddss
. (Úselo-march=native
para permitir el uso de cualquier conjunto de instrucciones que admita su CPU, no solo la línea base SSE2 para x86-64).Dos puntos a considerar:
El hardware moderno puede superponer instrucciones, ejecutarlas en paralelo y reordenarlas para hacer un mejor uso del hardware. Y también, es probable que cualquier programa de punto flotante significativo también tenga un trabajo de enteros significativo, incluso si solo está calculando índices en matrices, contador de bucles, etc., por lo que incluso si tiene una instrucción de punto flotante lenta, es posible que se esté ejecutando en un bit de hardware separado superpuesto con algunos de los trabajos enteros. Mi punto es que incluso si las instrucciones de punto flotante son lentas que las enteras, su programa general puede ejecutarse más rápido porque puede hacer uso de más hardware.
Como siempre, la única forma de estar seguro es perfilar su programa real.
El segundo punto es que la mayoría de las CPU en estos días tienen instrucciones SIMD para punto flotante que pueden operar en múltiples valores de punto flotante al mismo tiempo. Por ejemplo, puede cargar 4 flotantes en un solo registro SSE y realizar 4 multiplicaciones en todos ellos en paralelo. Si puede reescribir partes de su código para usar las instrucciones SSE, entonces parece probable que sea más rápido que una versión entera. Visual c ++ proporciona funciones intrínsecas del compilador para hacer esto; consulte http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx para obtener más información.
fuente
La versión de punto flotante será mucho más lenta, si no hay operación restante. Dado que todas las adiciones son secuenciales, la CPU no podrá paralelizar la suma. La latencia será crítica. La latencia de adición de FPU suele ser de 3 ciclos, mientras que la suma de enteros es de 1 ciclo. Sin embargo, el divisor para el operador restante probablemente será la parte crítica, ya que no está completamente integrado en las CPU modernas. por lo tanto, asumiendo que la instrucción dividir / restar consumirá la mayor parte del tiempo, la diferencia debida a la latencia de adición será pequeña.
fuente
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 enteros frente a la de punto flotante rara vez 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 puede implementar el algoritmo de dibujo de líneas usando números enteros y de punto flotante, llámelo una gran cantidad de veces y vea si hace una diferencia:
http://en.wikipedia.org/wiki/Bresenham's_algorithm
fuente
Hoy en día, las operaciones con números enteros suelen ser un poco más rápidas que las operaciones con coma flotante. Entonces, si puede hacer un cálculo con las mismas operaciones en entero y punto flotante, use integer. SIN EMBARGO está diciendo "Esto causa muchos problemas molestos y agrega mucho código molesto". Parece que necesita más operaciones porque usa aritmética de enteros en lugar de punto flotante. En ese caso, el punto flotante se ejecutará más rápido porque
tan pronto como necesite más operaciones con números enteros, probablemente necesite muchas más, por lo que la ligera ventaja de velocidad es más que consumida 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 la velocidad es crítica, puede dedicar más tiempo a optimizar el código.
fuente
Ejecuté una prueba que acaba de agregar 1 al número en lugar de rand (). Los resultados (en un x86-64) fueron:
fuente
Basado en ese "algo que he escuchado" tan confiable, en los viejos tiempos, el cálculo de números enteros era de 20 a 50 veces más rápido que el punto flotante, y en estos días es menos del doble de rápido.
fuente