Je fais un travail critique de performance en C ++, et nous utilisons actuellement des calculs entiers pour les problèmes qui sont intrinsèquement en virgule flottante parce que "c'est plus rapide". Cela cause beaucoup de problèmes ennuyeux et ajoute beaucoup de code ennuyeux.
Maintenant, je me souviens avoir lu comment les calculs en virgule flottante étaient si lents environ vers les 386 jours, où je crois (IIRC) qu'il y avait un co-processeur optionnel. Mais de nos jours, avec des processeurs exponentiellement plus complexes et puissants, cela ne fait aucune différence en termes de «vitesse» si vous faites un calcul en virgule flottante ou en nombre entier? Surtout que le temps de calcul réel est minime par rapport à quelque chose comme provoquer un blocage de pipeline ou récupérer quelque chose de la mémoire principale?
Je sais que la bonne réponse est de comparer le matériel cible, quel serait un bon moyen de tester cela? J'ai écrit deux petits programmes C ++ et comparé leur temps d'exécution avec "time" sur Linux, mais le temps d'exécution réel est trop variable (cela n'aide pas que je tourne sur un serveur virtuel). À moins de passer toute ma journée à exécuter des centaines de points de repère, à créer des graphiques, etc., puis-je faire quelque chose pour obtenir un test raisonnable de la vitesse relative? Des idées ou des pensées? Ai-je complètement tort?
Les programmes que j'ai utilisés comme suit, ils ne sont en aucun cas identiques:
#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;
}
Programme 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;
}
Merci d'avance!
Edit: La plate-forme qui me tient à cœur est x86 ou x86-64 standard fonctionnant sur des ordinateurs de bureau Linux et Windows.
Edit 2 (collé à partir d'un commentaire ci-dessous): Nous avons actuellement une base de code étendue. Vraiment, je me suis heurté à la généralisation selon laquelle «nous ne devons pas utiliser de flottant car le calcul des nombres entiers est plus rapide» - et je cherche un moyen (si c'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 sans faire tout le travail et le profiler par la suite.
Quoi qu'il en soit, merci pour toutes vos excellentes réponses et votre aide. N'hésitez pas à ajouter autre chose :).
la source
addl
remplacé parfadd
, par exemple). La seule façon d'obtenir une bonne mesure est d'obtenir une partie essentielle de votre programme réel et de profiler différentes versions de celui-ci. Malheureusement, cela peut être assez difficile sans utiliser des tonnes d'efforts. Peut-être que nous dire le matériel cible et votre compilateur aiderait au moins les gens à vous donner une expérience préexistante, etc. À propos de votre utilisation d'entiers, je suppose que vous pourriez créer une sorte defixed_point
classe de modèle qui faciliterait énormément ce travail.float
augmente la vitesse, mais cedouble
n'est généralement pas le cas.Réponses:
Hélas, je ne peux que vous donner une réponse "ça dépend" ...
D'après mon expérience, il existe de très nombreuses variables de performances ... en particulier entre les nombres entiers et flottants. Il varie fortement d'un processeur à l'autre (même au sein d'une même famille telle que x86) car différents processeurs ont des longueurs de «pipeline» différentes. De plus, certaines opérations sont généralement très simples (comme l'ajout) et ont un chemin accéléré à travers le processeur, et d'autres (comme la division) prennent beaucoup, beaucoup plus de temps.
L'autre grande variable est l'emplacement des données. Si vous n'avez que quelques valeurs à ajouter, toutes les données peuvent résider dans le cache, où elles peuvent être rapidement envoyées au processeur. Une opération en virgule flottante très, très lente qui contient déjà les données en cache sera plusieurs fois plus rapide qu'une opération entière où un entier doit être copié à partir de la mémoire système.
Je suppose que vous posez cette question parce que vous travaillez sur une application critique pour les performances. 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 considérablement 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, plus il existe une * banque de registres séparée 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 en fait les programmes, car ces anciennes instructions utilisaient en fait les mêmes registres que le FPU, ce qui rendait impossible d'utiliser à la fois le FPU et le MMX en même temps.
la source
double
-precision FP. Avec seulement deux 64 bitsdouble
par registre, l'accélération potentielle est inférieure à cellefloat
du code qui se vectorise bien. Scalairefloat
etdouble
utilise les registres XMM sur x86-64, avec l'ancien x87 utilisé uniquement pourlong double
. (Donc @ Dan: non, les registres MMX ne sont pas en conflit avec les registres FPU normaux, car le FPU normal sur x86-64 est l'unité SSE. MMX serait inutile car si vous pouvez faire un SIMD entier, vous voulez 16 octetsxmm0..15
au lieu de 8 -bytemm0..7
, et les processeurs modernes ont un débit MMX pire que SSE.)Par exemple (les nombres inférieurs sont plus rapides),
Intel Xeon X5550 64 bits à 2,67 GHz, gcc 4.1.2
-O3
Processeur AMD Opteron (tm) bicœur 32 bits 265 à 1,81 GHz, gcc 3.4.6
-O3
Comme l'a souligné Dan , même une fois que vous avez normalisé la fréquence d'horloge (ce qui peut être trompeur en soi dans les conceptions en pipeline), les résultats varieront énormément en fonction de l'architecture du processeur ( performances ALU / FPU individuelles , ainsi que du nombre réel d'ALU / FPU disponibles par core dans les conceptions superscalaires qui influence le nombre d' opérations indépendantes peuvent s'exécuter en parallèle - ce dernier facteur n'est pas exercé par le code ci-dessous car toutes les opérations ci-dessous sont séquentiellement dépendantes.)
Référence de fonctionnement FPU / ALU du pauvre:
la source
volatile
pour m'en assurer. Sur Win64, le FPU est inutilisé et MSVC ne génère pas de code pour cela, il compile l' aidemulss
et desdivss
instructions XMM là - bas, qui sont 25x plus vite que la FPU dans Win32. La machine de test est Core i5 M 520 à 2,40 GHzv
atteindront rapidement 0 ou +/- inf très rapidement, ce qui peut ou non être (théoriquement) traité comme un cas particulier / accéléré par certaines implémentations de fpu.v
). Sur les récentes conceptions Intel, la division n'est pas du tout en pipeline (divss
/divps
a une latence de 10 à 14 cycles et le même débit réciproque).mulss
cependant, la latence est de 5 cycles, mais peut en émettre un à chaque cycle. (Ou deux par cycle sur Haswell, puisque le port 0 et le port 1 ont tous deux un multiplicateur pour FMA).Il y aura probablement une différence significative de vitesse dans le monde réel entre les mathématiques en virgule fixe et en virgule flottante, mais le débit théorique dans le meilleur des cas de l'ALU par rapport au FPU est totalement 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 autrement utilisés par votre calcul (par exemple pour le contrôle de boucle), le nombre d'éléments de chaque type qui tiennent dans une ligne de cache , optimisations possibles compte tenu des différentes sémantiques pour les nombres entiers et flottants - 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 performance sur votre problème.
Par exemple, l'addition d'entiers est commutative, donc si le compilateur voit une boucle comme vous l'avez utilisée pour un benchmark (en supposant que les données aléatoires ont été préparées à l'avance pour ne pas obscurcir les résultats), il peut dérouler la boucle et calculer des sommes partielles avec pas de dépendances, puis ajoutez-les lorsque la boucle se termine. Mais avec la virgule flottante, le compilateur doit effectuer les opérations dans le même ordre que vous avez demandé (vous avez des points de séquence là-dedans donc le compilateur doit garantir le même résultat, ce qui interdit la réorganisation) donc il y a une forte dépendance de chaque ajout sur le résultat du précédent.
Il est probable que vous placiez également plus d'opérandes entiers dans le cache à la fois. Ainsi, la version à virgule fixe pourrait surpasser la version flottante d'un ordre de grandeur, même sur une machine où le FPU a un débit théoriquement plus élevé.
la source
L'ajout est beaucoup plus rapide que
rand
, donc votre programme est (surtout) inutile.Vous devez identifier les points chauds de performances et modifier progressivement votre programme. Il semble que vous ayez des problèmes avec votre environnement de développement qui devront d'abord être résolus. Est-il impossible d'exécuter votre programme sur votre PC pour un petit ensemble de problèmes?
En règle générale, la tentative de travaux FP avec arithmétique entière est une recette pour la lenteur.
la source
timespec_t
ou quelque chose de similaire. Enregistrez l'heure au début et à la fin de la boucle et faites la différence. Déplacez ensuite larand
génération de données hors de la boucle. Assurez-vous que votre algorithme obtient toutes ses données à partir de tableaux et place toutes ses données dans des tableaux. Cela obtient votre algorithme réel par lui-même, et obtient la configuration, malloc, l'impression des résultats, tout sauf la commutation de tâches et interrompt votre boucle de profilage.TIL Cela varie (beaucoup). Voici quelques résultats en utilisant le compilateur gnu (btw j'ai également vérifié en compilant sur des machines, gnu g ++ 5.4 de xenial est beaucoup plus rapide que 4.6.3 de linaro avec précision)
Intel i7 4700MQ xenial
Intel i3 2370M a des résultats similaires
Intel (R) Celeron (R) 2955U (Chromebook Acer C720 exécutant xenial)
Processeur Intel (R) Xeon (R) E5-2630L v2 (en cours d'exécution Trusty) DigitalOcean 1 Go Droplet
Processeur AMD Opteron (tm) 4122 (précis)
Cela utilise le code de http://pastebin.com/Kx8WGUfg comme
benchmark-pc.c
J'ai exécuté plusieurs passes, mais cela semble être le cas où 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 ce qui précède sous forme de graphique (cliquez pour la taille réelle, le bas est plus rapide et préférable):
Mettre à jour pour accueillir @Peter Cordes
https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc
i7 4700MQ Linux Ubuntu Xenial 64 bits (tous les correctifs du 13/03/2018 appliqués) Processeur AMD Opteron (tm) 4122 (précis, hébergement partagé DreamHost) Intel Xeon E5-2630L v2 à 2,4 GHz (Trusty 64 bits, DigitalOcean VPS)la source
benchmark-pc
mesure d' une combinaison de débit et de latence? Sur votre Haswell (i7 4700MQ), la multiplication des nombres entiers est de 1 par débit d'horloge, 3 cycles de latence, mais les entiers add / sub est de 4 par débit d'horloge, 1 temps de latence ( agner.org/optimize ). Donc, vraisemblablement, il y a beaucoup de surcharge de boucle diluant ces nombres pour que add et mul soient si proches (long add: 0.824088 vs long mul: 1.017164). (par défaut, gcc ne déroule pas les boucles, sauf pour le déroulement complet de très faibles nombres d'itérations).int
, seulementshort
etlong
? Sur Linux x86-64,short
est de 16 bits (et a donc des ralentissements de registre partiel dans certains cas), tandis quelong
etlong long
sont tous les deux de type 64 bits. (Peut-être est-il conçu pour Windows où x86-64 utilise toujours 32 bitslong
? Ou peut-être est-il conçu pour le mode 32 bits.) Sous Linux, l'ABI x32 a 32 bitslong
en mode 64 bits , donc si vous avez les bibliothèques installées , utilisezgcc -mx32
pour compilateur pour ILP32. Ou utilisez simplement-m32
et regardez leslong
chiffres.addps
registres xmm au lieu deaddss
, pour faire 4 FP ajoute en parallèle dans une instruction qui est aussi rapide que scalaireaddss
. (Utilisez-march=native
pour autoriser l'utilisation de tous les jeux d'instructions pris en charge par votre processeur, pas seulement la ligne de base SSE2 pour x86-64).Deux points à considérer -
Le matériel moderne peut chevaucher des instructions, les exécuter en parallèle et les réorganiser pour tirer le meilleur parti du matériel. Et aussi, tout programme à virgule flottante significatif est susceptible d'avoir un travail d'entier significatif même s'il ne calcule que des indices dans des tableaux, un compteur de boucles, etc. donc même si vous avez une instruction à virgule flottante lente, il peut bien fonctionner sur un bit séparé de matériel. chevauché avec une partie du travail entier. Mon point est que même si les instructions en virgule flottante sont lentes que les instructions entières, votre programme global peut s'exécuter plus rapidement car il peut utiliser plus de matériel.
Comme toujours, la seule façon d'être sûr est de profiler votre programme actuel.
Le deuxième point est que la plupart des processeurs de nos jours ont des instructions SIMD pour la virgule flottante qui peuvent fonctionner sur plusieurs valeurs à virgule flottante en même temps. Par exemple, vous pouvez charger 4 flotteurs dans un seul registre SSE et effectuer 4 multiplications sur eux tous en parallèle. Si vous pouvez réécrire des parties de votre code pour utiliser les instructions SSE, il semble probable que ce sera plus rapide qu'une version entière. Visual C ++ fournit des fonctions intrinsèques au compilateur pour ce faire, consultez http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx pour plus d'informations.
la source
La version en virgule flottante sera beaucoup plus lente, s'il n'y a pas d'opération de reste. Puisque toutes les additions sont séquentielles, 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 diviseur pour l'opérateur de reste sera probablement la partie critique, car il n'est pas entièrement pipeliné sur les processeurs modernes. donc, en supposant que l'instruction de division / reste consommera la majeure partie du temps, la différence due à l'ajout de latence sera faible.
la source
À moins que vous n'écriviez du code qui sera appelé des millions de fois par seconde (comme, par exemple, dessiner une ligne sur l'écran dans une application graphique), l'arithmétique des nombres entiers par rapport aux flottants est rarement le goulot d'étranglement.
La première étape habituelle des questions d'efficacité consiste à profiler votre code pour voir où le temps d'exécution est réellement passé. La commande linux pour cela est
gprof
.Éditer:
Bien que je suppose que vous pouvez toujours implémenter l'algorithme de dessin de ligne en utilisant des 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 source
Aujourd'hui, les opérations sur les entiers sont généralement un peu plus rapides que les opérations en virgule flottante. Donc, si vous pouvez faire un calcul avec les mêmes opérations en entier et en virgule flottante, utilisez entier. CEPENDANT, vous dites "cela cause beaucoup de problèmes ennuyeux et ajoute beaucoup de code ennuyeux". Il semble que vous ayez besoin de plus d'opérations car vous utilisez l'arithmétique d'entiers au lieu de virgule flottante. Dans ce cas, la virgule flottante fonctionnera plus rapidement car
dès que vous avez besoin de plus d'opérations entières, vous en avez probablement besoin de beaucoup plus, donc le léger avantage de vitesse est plus que rongé par 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 s'il est critique en termes de vitesse, vous pouvez passer plus de temps à optimiser le code.
la source
J'ai exécuté un test qui vient d'ajouter 1 au nombre au lieu de rand (). Les résultats (sur un x86-64) étaient:
la source
Sur la base de ce "quelque chose que j'ai entendu" si fiable, à l'époque, le calcul des nombres entiers était environ 20 à 50 fois plus rapide que la virgule flottante, et de nos jours, il est moins de deux fois plus rapide.
la source