Calculs en virgule flottante contre nombre entier sur matériel moderne

Je fais un travail de performance critique en C ++, et nous utilisons actuellement des calculs entiers pour des problèmes qui sont insortingnsèquement en virgule flottante car “c’est plus rapide”. Cela provoque beaucoup de problèmes ennuyeux et ajoute beaucoup de code ennuyeux.

Maintenant, je me souviens avoir lu à quel point les calculs en virgule flottante étaient si lents environ environ 386 jours, où je crois (IIRC) qu’il y avait un co-procureur optionnel. Mais de nos jours, avec des processeurs exponentiellement plus complexes et plus puissants, cela ne fait aucune différence en termes de “vitesse” en cas de calcul en virgule flottante ou en nombre entier? Surtout que le temps de calcul réel est minime comparé à quelque chose comme provoquer un blocage de pipeline ou aller chercher quelque chose dans la mémoire principale?

Je sais que la bonne réponse est de comparer le matériel cible, quel serait un bon moyen de le tester? J’ai écrit deux minuscules programmes C ++ et comparé leur temps d’exécution avec “time” sous Linux, mais le temps d’exécution réel est trop variable (cela ne m’aide pas à fonctionner sur un serveur virtuel). Sans passer toute ma journée à exécuter des centaines de tests, créer des graphiques, etc., est-ce que je peux faire quelque chose pour obtenir un test raisonnable de la vitesse relative? Des idées ou des pensées? Est-ce que j’ai complètement tort?

Les programmes que j’ai utilisés comme suit ne sont en aucun cas identiques:

#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; } 

Programme 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; } 

Merci d’avance!

Edit: La plate-forme qui m’intéresse est la version x86 ou x86-64 standard qui s’exécute sur les ordinateurs de bureau Linux et Windows.

Edit 2 (collé à partir d’un commentaire ci-dessous): Nous avons actuellement une base de code complète. Vraiment, je me suis heurté à la généralisation que nous “ne devons pas utiliser float car le calcul des nombres entiers est plus rapide” – et je cherche un moyen (si cela est même vrai) de réfuter cette hypothèse généralisée. Je me rends compte qu’il serait impossible de prédire le résultat exact pour nous, à moins de faire tout le travail et de le profiler par la suite.

En tout cas, merci pour vos excellentes réponses et votre aide. N’hésitez pas à append autre chose :).

Hélas, je ne peux que vous donner une réponse “ça dépend” …

De mon expérience, il y a beaucoup de variables à la performance … en particulier entre les nombres entiers et les nombres à virgule flottante. Il varie fortement d’un processeur à l’autre (même au sein d’une même famille telle que x86) car les processeurs ont des longueurs de “pipeline” différentes. En outre, certaines opérations sont généralement très simples (telles que l’addition) et ont un cheminement accéléré dans le processeur, tandis que d’autres (comme la division) prennent beaucoup plus de temps.

L’autre grande variable est l’endroit où les données résident. Si vous n’avez que quelques valeurs à append, toutes les données peuvent résider dans le cache, où elles peuvent être rapidement envoyées au processeur. Une opération très lente en virgule flottante dont les données sont déjà en cache sera beaucoup plus rapide qu’une opération entière où un entier doit être copié de la mémoire système.

Je suppose que vous posez cette question car vous travaillez sur une application critique. Si vous développez pour l’architecture x86 et que vous avez besoin de performances supplémentaires, vous pouvez envisager d’utiliser les extensions SSE. Cela peut grandement accélérer l’arithmétique à virgule flottante simple précision, car la même opération peut être effectuée sur plusieurs données à la fois, en plus d’une banque de registres * distincte pour les opérations SSE. (J’ai remarqué dans votre deuxième exemple que vous utilisiez “float” au lieu de “double”, ce qui me fait penser que vous utilisez des mathématiques à simple précision).

* Remarque: L’utilisation des anciennes instructions MMX ralentirait réellement les programmes, car ces anciennes instructions utilisaient en fait les mêmes registres que la FPU, ce qui rend impossible l’utilisation simultanée de la FPU et de MMX.

Par exemple (les nombres inférieurs sont plus rapides),

Intel Xeon X5550 64 bits à 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] 

Processeur AMD Opteron ™ 32 bits Dual Core 265 à 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] 

Comme Dan l’a souligné , même une fois la fréquence d’horloge normalisée (ce qui peut induire en erreur dans les conceptions en pipeline), les résultats varieront énormément en fonction de l’architecture du processeur ( performances ALU / FPU individuelles et nombre réel d’ALU / FPU disponibles). core dans les conceptions superscalaires qui influence le nombre d’ opérations indépendantes pouvant être exécutées en parallèle – le code ci-dessous n’exerce pas ce dernier facteur car toutes les opérations ci-dessous dépendent de manière séquentielle.)

Le sharepoint référence de l’opération FPU / ALU de l’homme pauvre:

 #include  #ifdef _WIN32 #include  #else #include  #endif #include  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; } 

Il y a probablement une différence significative dans la vitesse réelle entre les calculs à virgule fixe et à virgule flottante, mais le meilleur rendement théorique de l’ALU par rapport à la FPU est complètement hors de propos. Au lieu de cela, le nombre de registres entiers et à virgule flottante (registres réels, pas de noms de registres) sur votre architecture qui ne sont pas utilisés par votre calcul (par exemple pour le contrôle de boucle), le nombre d’éléments de chaque type , optimisations possibles en tenant compte de la sémantique différente pour les maths en nombres entiers et en virgule flottante – ces effets domineront. Les dépendances de données de votre algorithme jouent ici un rôle important, de sorte qu’aucune comparaison générale ne prédira l’écart de performances sur votre problème.

Par exemple, l’addition d’entiers est commutative, donc si le compilateur voit une boucle comme celle utilisée pour un benchmark (en supposant que les données aléatoires ont été préparées à l’avance pour ne pas masquer les résultats), il peut dérouler la boucle et calculer des sums partielles avec aucune dépendance, puis ajoutez-les lorsque la boucle se termine. Mais avec le virgule flottante, le compilateur doit effectuer les opérations dans le même ordre que celui que vous avez demandé (vous avez des points de séquence pour que le compilateur garantisse le même résultat, ce qui interdit le réordonnancement). le résultat du précédent.

Vous êtes susceptible de placer plus d’opérandes entières dans le cache à la fois. Ainsi, la version à virgule fixe pourrait surpasser la version à flotteur d’un ordre de grandeur, même sur une machine où le débit du FPU est théoriquement supérieur.

L’ajout est beaucoup plus rapide que le rand , donc votre programme est (surtout) inutile.

Vous devez identifier les zones sensibles des performances et modifier de manière incrémentielle votre programme. Il semble que vous ayez des problèmes avec votre environnement de développement qui devront être résolus en premier. Est-il impossible de lancer votre programme sur votre PC pour régler un petit problème?

En règle générale, la tentative de travaux FP avec l’arithmétique entière est une recette pour ralentir.

TIL Cela varie (beaucoup). Voici quelques résultats avec le compilateur gnu (btw j’ai aussi vérifié en compilant sur des machines, gnu g ++ 5.4 de xenial est bien plus rapide que 4.6.3 de linaro sur le précis)

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 a des résultats similaires

 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 exécutant 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 Droplet 1 Go Intel (R) Xeon (R) CPU E5-2630L v2 (en cours d’exécution)

 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 

Processeur AMD Opteron ™ 4122 (précis)

 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 

Ceci utilise le code de http://pastebin.com/Kx8WGUfg comme benchmark-pc.c

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

J’ai effectué plusieurs passes, mais cela semble être le cas si les chiffres généraux sont les mêmes.

Une exception notable semble être ALU mul vs FPU mul. L’addition et la soustraction semblent très différentes.

Voici la forme ci-dessus sous forme de graphique (cliquez pour agrandir, plus bas est plus rapide et préférable):

Tableau des données ci-dessus

Mise à jour pour accueillir @Peter Cordes

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

i7 4700MQ Linux Ubuntu Xenial 64-bit (tous les correctifs à 2018-03-13 appliqués)

  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 

Processeur AMD Opteron ™ 4122 (précis, hébergement partagé 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 à 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 

Deux points à considérer –

Le matériel moderne peut chevaucher des instructions, les exécuter en parallèle et les réorganiser pour optimiser l’utilisation du matériel. Et aussi, tout programme significatif en virgule flottante est susceptible d’avoir un travail entier significatif même s’il ne fait que calculer des index dans des tableaux, des compteurs de boucles, etc. Même si vous avez une instruction en virgule flottante lente chevauché avec une partie du travail en nombre entier. Ce que je veux dire, c’est que même si les instructions en virgule flottante sont lentes par rapport aux entiers, votre programme global peut fonctionner plus rapidement car il peut utiliser davantage de matériel.

Comme toujours, la seule manière d’être sûr est de définir votre programme actuel.

Le deuxième point est que la plupart des processeurs ont de nos jours des instructions SIMD pour les virgules flottantes qui peuvent fonctionner sur plusieurs valeurs à virgule flottante simultanément. Par exemple, vous pouvez charger 4 flottants dans un seul registre SSE et les effectuer 4 en parallèle. Si vous pouvez réécrire des parties de votre code pour utiliser les instructions SSE, il semble probable que cela sera plus rapide qu’une version entière. Visual c ++ fournit des fonctions insortingnsèques au compilateur pour ce faire, consultez http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx pour plus d’informations.

J’ai effectué un test qui vient d’append 1 au nombre au lieu de rand (). Les résultats (sur un x86-64) étaient les suivants:

  • court: 4.260s
  • int: 4.020s
  • long long: 3.350s
  • flotteur: 7.330s
  • double: 7.210s

À moins d’écrire du code qui sera appelé des millions de fois par seconde (par exemple, dessiner une ligne à l’écran dans une application graphique), l’arithmétique entière ou à virgule flottante est rarement le goulot d’étranglement.

La première étape habituelle pour les questions d’efficacité consiste à définir votre code pour voir où le temps d’exécution est réellement dépensé. La commande linux pour cela est gprof .

Modifier:

Bien que je suppose que vous pouvez toujours implémenter l’algorithme de dessin au trait en utilisant des nombres entiers et des nombres à virgule flottante, appelez-le un grand nombre de fois et voyez si cela fait une différence:

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

La version à virgule flottante sera beaucoup plus lente s’il n’y a pas d’opération restante. Comme tous les ajouts sont séquentiels, le processeur ne pourra pas paralléliser la sommation. La latence sera critique. La latence d’ajout de FPU est généralement de 3 cycles, tandis que l’addition d’entier est de 1 cycle. Cependant, le séparateur pour l’opérateur restant constituera probablement la partie critique, car il n’est pas entièrement intégré aux processeurs modernes. donc, en supposant que l’instruction diviser / rest consum l’essentiel du temps, la différence due à l’ajout de latence sera faible.

Aujourd’hui, les opérations entières sont généralement un peu plus rapides que les opérations à virgule flottante. Donc, si vous pouvez faire un calcul avec les mêmes opérations en entier et en virgule flottante, utilisez un entier. Cependant, vous dites “Cela cause beaucoup de problèmes et ajoute beaucoup de code”. Cela semble avoir besoin de plus d’opérations car vous utilisez l’arithmétique entière au lieu de la virgule flottante. Dans ce cas, la virgule flottante s’exécutera plus rapidement car

  • dès que vous avez besoin de plus d’opérations sur les nombres entiers, vous avez probablement besoin de beaucoup plus, de sorte que le léger avantage de la vitesse est plus important que les opérations supplémentaires

  • le code à virgule flottante est plus simple, ce qui signifie qu’il est plus rapide d’écrire le code, ce qui signifie que si la vitesse est critique, vous pouvez passer plus de temps à optimiser le code.

Sur la base de ce «quelque chose que j’ai entendu», jadis fiable, le calcul en nombre entier était environ 20 à 50 fois plus rapide en virgule flottante, et de nos jours, il est deux fois moins rapide.