Cálculos de punto flotante vs entero en hardware moderno

100

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 :).

maxpenguin
fuente
8
Lo que tienes ahora como prueba es trivial. Probablemente también haya muy poca diferencia en el ensamblaje ( addlreemplazado por fadd, 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 de fixed_pointplantilla que facilitaría enormemente ese trabajo.
GManNickG
1
Todavía hay muchas arquitecturas que no tienen hardware de punto flotante dedicado; algunas etiquetas que explican los sistemas que le interesan le ayudarán a obtener mejores respuestas.
Carl Norum
3
Creo que el hardware de mi HTC Hero (Android) no tiene FPU, pero el hardware de Google NexusOne (Android) sí. cual es tu objetivo PC de escritorio / servidor? netbooks (posible arm + linux)? ¿Los telefonos?
SteelBytes
5
Si desea FP rápido en x86, intente compilar con optimización y generación de código SSE. SSE (cualquier versión) puede hacer al menos sumar, restar y multiplicar en un solo ciclo. Las funciones de división, modificación y superior siempre serán lentas. También tenga en cuenta que floatobtiene el aumento de velocidad, pero generalmente doubleno lo hace.
Mike D.
1
El entero de punto fijo se aproxima a FP mediante el uso de múltiples operaciones de entero para evitar que los resultados se desborden. Eso es casi siempre más lento que simplemente usar las FPU extremadamente capaces que se encuentran en las CPU de escritorio modernas. Por ejemplo, MAD, el decodificador de mp3 de punto fijo, es más lento que libmpg123, y aunque es de buena calidad para un decodificador de punto fijo, libmpg123 todavía tiene menos errores de redondeo. wezm.net/technical/2008/04/mp3-decoder-libraries-comparado para los puntos de referencia en un PPC G5.
Peter Cordes

Respuestas:

35

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.

Dan
fuente
8
Y en algunos procesadores, las matemáticas de FP pueden ser más rápidas que las de números enteros. El procesador Alpha tenía una instrucción FP de división pero no una entera, por lo que la división de enteros tenía que hacerse en software.
Gabe
¿SSEx también acelerará la aritmética de doble precisión? Lo siento, no estoy muy familiarizado con SSE
Johannes Schaub - litb
1
@ JohannesSchaub-litb: SSE2 (línea de base para x86-64) ha empaquetado double-precision FP. Con solo dos de 64 bits doublepor registro, la aceleración potencial es menor que floatpara el código que se vectoriza bien. Escalar floaty doubleusar registros XMM en x86-64, con x87 heredado solo usado para long 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 en xmm0..15lugar de 8 -byte mm0..7, y las CPU modernas tienen peor rendimiento MMX que SSE.)
Peter Cordes
1
Pero las instrucciones enteras MMX y SSE * / AVX2 compiten por las mismas unidades de ejecución, por lo que usar ambas a la vez casi nunca es útil. Simplemente use las versiones XMM / YMM más amplias para hacer más trabajo. El uso de SIMD integer y FP al mismo tiempo compite por los mismos registros, pero x86-64 tiene 16 de ellos. Pero los límites de rendimiento total significan que no puede realizar el doble de trabajo utilizando unidades de ejecución de números enteros y FP en paralelo.
Peter Cordes
49

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

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) de doble núcleo de 32 bits 265 a 1,81 GHz, 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 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:

#include <stdio.h>
#ifdef _WIN32
#include <sys/timeb.h>
#else
#include <sys/time.h>
#endif
#include <time.h>
#include <cstdlib>

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;
}
vladr
fuente
8
¿Por qué mezclaron mult y div? ¿No debería ser interesante si mult es quizás (¿o se esperaba?) Mucho más rápido que div?
Kyss Tao
13
La multiplicación es mucho más rápida que la división tanto en el caso de números enteros como de coma flotante. El rendimiento de la división también depende del tamaño de los números. Por lo general, asumo que la división es ~ 15 veces más lenta.
Sogartar
4
pastebin.com/Kx8WGUfg Tomé su punto de referencia y separé cada operación en su propio bucle y agregué volatilepara asegurarme. En Win64, la FPU no se utiliza y MSVC no generará código para ello, por lo que compila utilizando mulssy divsslas 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.40GHz
James Dunne
4
@JamesDunne solo tenga cuidado, ya que las operaciones de fp valcanzará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.
vladr
3
Este "punto de referencia" no tiene paralelismo de datos para la ejecución fuera de orden, porque cada operación se realiza con el mismo acumulador ( v). En los diseños recientes de Intel, la división no se canaliza en absoluto ( divss/ divpstiene una latencia de 10-14 ciclos y el mismo rendimiento recíproco). mulsssin 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).
Peter Cordes
23

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.

Ben Voigt
fuente
4
+1 por señalar cómo los puntos de referencia ingenuos pueden producir bucles de tiempo 0 debido a operaciones enteras constantes desenrolladas. Además, el compilador puede descartar completamente el bucle (entero o FP) si el resultado no se usa realmente.
vladr
La conclusión a eso es: uno debe llamar a una función que tenga la variable de bucle como argumento. Dado que creo que ningún compilador podría ver que la función no hace nada y que la llamada puede ignorarse. Dado que hay una sobrecarga de llamada, solo las diferencias de tiempo == (tiempo flotante - tiempo entero) serán significativas.
GameAlchemist
@GameAlchemist: muchos compiladores eliminan las llamadas a funciones vacías, como efecto secundario de la inserción. Tienes que hacer un esfuerzo para evitarlo.
Ben Voigt
El OP sonaba como si estuviera hablando de usar un número entero para cosas donde FP sería un ajuste más natural, por lo que se necesitaría más código entero para lograr el mismo resultado que el código FP. En este caso, simplemente use FP. Por ejemplo, en hardware con una FPU (por ejemplo, una CPU de escritorio), los decodificadores MP3 enteros de coma fija son más lentos (y ligeramente más errores de redondeo) que los decodificadores de coma flotante. Las implementaciones de códecs de punto fijo existen principalmente para ejecutarse en CPU ARM reducidas sin hardware FP, solo FP lento emulado.
Peter Cordes
un ejemplo para el primer punto: en x86-64 con AVX-512 solo hay 16 registros GP pero 32 registros zmm, por lo que las matemáticas de punto flotante escalar pueden ser más rápidas
phuclv
18

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.

Potatoswatter
fuente
Sí, así como la conversión de un número entero en rand a un flotante en la versión de punto flotante. ¿Alguna idea sobre una mejor manera de probar esto?
maxpenguin
1
Si está tratando de perfilar la velocidad, mire POSIX timespec_to algo similar. Registre el tiempo al principio y al final del ciclo y tome la diferencia. Luego mueva la randgeneració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.
Mike D.
3
@maxpenguin: la pregunta es qué estás probando. Artem ha asumido que estás haciendo gráficos, Carl consideró si estás en una plataforma incrustada sin FP, supuse que estás codificando ciencia para un servidor. No se pueden generalizar o "escribir" puntos de referencia. Los puntos de referencia se extraen del trabajo real que realiza su programa. Una cosa que puedo decirle es que no permanecerá "esencialmente a la misma velocidad" si toca el elemento crítico para el rendimiento en su programa, sea lo que sea.
Potatoswatter
buen punto y buena respuesta. Actualmente tenemos una amplia base de código. 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 tu ayuda.
maxpenguin
18

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

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 CPU Intel (R) Xeon (R) Droplet de 1GB E5-2630L v2 (funcionamiento confiable)

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

Esto usa código de http://pastebin.com/Kx8WGUfg comobenchmark-pc.c

g++ -fpermissive -O3 -o benchmark-pc 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):

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 aplicaron todos los parches para 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 (preciso, alojamiento compartido DreamHost)
    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 a 2,4 GHz (Trusty 64 bits, DigitalOcean 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
MrMesees
fuente
gcc5 ¿quizás auto-vectoriza algo que gcc4.6 no hizo? ¿Está benchmark-pcmidiendo 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).
Peter Cordes
Y por cierto, ¿por qué no prueba int, solo shorty long? En Linux x86-64, shortes de 16 bits (y por lo tanto tiene ralentizaciones de registro parcial en algunos casos), mientras que longy long longson ambos tipos de 64 bits. (¿Quizás esté diseñado para Windows, donde x86-64 todavía usa 32 bits long? O quizás está diseñado para el modo de 32 bits). En Linux, el ABI x32 tiene 32 bits longen modo de 64 bits , así que si tiene las bibliotecas instaladas , se usa gcc -mx32para compilar para ILP32. O simplemente usa -m32y mira los longnúmeros.
Peter Cordes
Y realmente debería comprobar si su compilador auto-vectorizó algo. por ejemplo, usar addpsen registros xmm en lugar de addsshacer 4 adiciones FP en paralelo en una instrucción que es tan rápida como escalar addss. (Úselo -march=nativepara permitir el uso de cualquier conjunto de instrucciones que admita su CPU, no solo la línea base SSE2 para x86-64).
Peter Cordes
@cincodenada, por favor, deje los gráficos que muestran los 15 completos al costado, ya que son ilustrativos del rendimiento.
MrMesees
@PeterCordes Intentaré mirar mañana, gracias por su diligencia.
MrMesees
7

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.

jcoder
fuente
Cabe señalar que en Win64, el compilador MSVC ya no genera las instrucciones FPU. El punto flotante siempre usa instrucciones SIMD allí. Esto genera una gran discrepancia de velocidad entre Win32 y Win64 con respecto a los flops.
James Dunne
5

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.

Goran D
fuente
4

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

Artem Sokolov
fuente
2
Las aplicaciones científicas utilizan FP. La única ventaja de FP es que la precisión es invariante en escala. Es como una notación científica. Si ya conoce la escala de los números (por ejemplo, que la longitud de la línea es un número de píxeles), se obvia FP. Pero antes de llegar a trazar la línea, eso no es cierto.
Potatoswatter
4

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.

gnasher729
fuente
Hay mucha especulación salvaje aquí, sin tener en cuenta ninguno de los efectos secundarios presentes en el hardware, que a menudo dominan el tiempo de cálculo. No es un mal punto de partida, pero debe comprobarse en cada aplicación en particular mediante la elaboración de perfiles y no enseñarse como un evangelio.
Ben Voigt
3

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
  • largo largo: 3.350 s
  • flotador: 7.330 s
  • doble: 7.210s
dan04
fuente
1
¿Fuente, opciones de compilación y método de sincronización? Estoy un poco sorprendido por los resultados.
GManNickG
Mismo bucle que OP con "rand ()% 365" reemplazado por "1". Sin optimización. Tiempo de usuario desde el comando "tiempo".
dan04
13
"Sin optimización" es la clave. Nunca perfila con la optimización desactivada, siempre perfila en modo "liberación".
Dean Harding
2
En este caso, sin embargo, la optimización obliga a que ocurra la operación, y se hace deliberadamente: el ciclo está ahí para dilatar el tiempo a una escala de medición razonable. El uso de la constante 1 elimina el costo de rand (). Un compilador de optimización suficientemente inteligente vería 1 agregado 100,000,000 de veces sin salida del bucle y simplemente agregaría 100000000 en una sola operación. Eso evita todo el propósito, ¿no?
Stan Rogers
7
@Stan, haz que la variable sea volátil. Incluso un compilador de optimización inteligente debería respetar las múltiples operaciones.
vladr
0

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.

James Curran
fuente
1
Considere mirar esto nuevamente ofreciendo más que una opinión (especialmente dado que la opinión parece ir en contra de los hechos recopilados)
MrMesees
1
@MrMesees Si bien esta respuesta no es muy útil, diría que es consistente con las pruebas que realizó. Y la trivia histórica probablemente también esté bien.
Jonatan Öström
Como alguien que trabajó con 286 en su día, puedo confirmar; "¡Sí ellos estaban!"
David H Parry