c++ - online - Cálculo de punto flotante vs entero en hardware moderno
punto flotante normalizado (11)
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:
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 bloqueo de la tubería o recuperar algo de la memoria principal?
Sé que la respuesta correcta es comparar el hardware objetivo, ¿cuál sería una buena manera de probar esto? Escribí dos pequeños programas 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 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 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 :).
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.
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 programa 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 programa 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 programa 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 compilador 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
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.
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 arquitectura 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 suma entera es conmutativa, por lo que si el compilador 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 sumas parciales con sin dependencias, luego agréguelas cuando termine el ciclo. Pero con punto flotante, el compilador tiene que hacer las operaciones en el mismo orden que usted solicitó (usted tiene puntos de secuencia allí para que el compilador 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 programa es (especialmente) inútil.
Necesita identificar puntos de acceso de rendimiento y modificar de forma incremental su programa. Parece que tienes problemas con tu entorno de desarrollo que primero tendrás que resolver. ¿Es imposible ejecutar su programa en su PC para un pequeño conjunto de problemas?
Generalmente, intentar trabajos de FP con aritmética de enteros es una receta lenta.
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 suma. La latencia será crítica. La latencia de agregación de FPU suele ser de 3 ciclos, mientras que la suma 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.
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 arquitectura 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 programas, 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 (tm) 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 arquitectura de la CPU ( rendimiento ALU / FPU individual, así como el número real de ALU / FPU disponibles por núcleo en diseños superscalar 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 <stdio.h>
#ifdef _WIN32
#include <sys/timeb.h>
#else
#include <sys/time.h>
#endif
#include <time.h>
double
mygettime(void) {
# ifdef _WIN32
struct _timeb tb;
_ftime(&tb);
return (double)tb.time + (0.001 * (double)tb.millitm);
# else
struct timeval tv;
if(gettimeofday(&tv, 0) < 0) {
perror("oops");
}
return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
# endif
}
template< typename Type >
void my_test(const char* name) {
Type v = 0;
// Do not use constants or repeating values
// to avoid loop unroll optimizations.
// All values >0 to avoid division by 0
// Perform ten ops/iteration to reduce
// impact of ++i below on measurements
Type v0 = (Type)(rand() % 256)/16 + 1;
Type v1 = (Type)(rand() % 256)/16 + 1;
Type v2 = (Type)(rand() % 256)/16 + 1;
Type v3 = (Type)(rand() % 256)/16 + 1;
Type v4 = (Type)(rand() % 256)/16 + 1;
Type v5 = (Type)(rand() % 256)/16 + 1;
Type v6 = (Type)(rand() % 256)/16 + 1;
Type v7 = (Type)(rand() % 256)/16 + 1;
Type v8 = (Type)(rand() % 256)/16 + 1;
Type v9 = (Type)(rand() % 256)/16 + 1;
double t1 = mygettime();
for (size_t i = 0; i < 100000000; ++i) {
v += v0;
v -= v1;
v += v2;
v -= v3;
v += v4;
v -= v5;
v += v6;
v -= v7;
v += v8;
v -= v9;
}
// Pretend we make use of v so compiler doesn''t optimize out
// the loop completely
printf("%s add/sub: %f [%d]/n", name, mygettime() - t1, (int)v&1);
t1 = mygettime();
for (size_t i = 0; i < 100000000; ++i) {
v /= v0;
v *= v1;
v /= v2;
v *= v3;
v /= v4;
v *= v5;
v /= v6;
v *= v7;
v /= v8;
v *= v9;
}
// Pretend we make use of v so compiler doesn''t optimize out
// the loop completely
printf("%s mul/div: %f [%d]/n", name, mygettime() - t1, (int)v&1);
}
int main() {
my_test< short >("short");
my_test< long >("long");
my_test< long long >("long long");
my_test< float >("float");
my_test< double >("double");
return 0;
}
TIL Esto varía (mucho). Aquí hay algunos resultados usando el compilador gnu (por cierto, también lo comprobé 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
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 (tm) 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 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 suma 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):
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 (tm) 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