Calculs en virgule flottante vs nombres entiers sur du matériel moderne

100

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

maxpenguin
la source
8
Ce que vous avez maintenant comme test est trivial. Il y a aussi probablement très peu de différence dans l'assemblage ( addlremplacé par fadd, 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 de fixed_pointclasse de modèle qui faciliterait énormément ce travail.
GManNickG
1
Il existe encore de nombreuses architectures qui n'ont pas de matériel dédié en virgule flottante - certaines balises expliquant les systèmes qui vous intéressent vous aideront à obtenir de meilleures réponses.
Carl Norum
3
Je pense que le matériel de mon HTC Hero (Android) n'a pas de FPU, mais le matériel de Google NexusOne (Android) en a. quel est votre objectif? PC de bureau / serveur? netbooks (arm + linux possible)? Téléphone (s?
SteelBytes
5
Si vous voulez une FP rapide sur x86, essayez de compiler avec l'optimisation et la génération de code SSE. SSE (quelle que soit la version) peut faire au moins ajouter, soustraire et multiplier en un seul cycle. Les fonctions de division, de modification et supérieures seront toujours lentes. Notez également que cela floataugmente la vitesse, mais ce doublen'est généralement pas le cas.
Mike D.
1
L'entier à virgule fixe se rapproche de FP en utilisant plusieurs opérations sur les entiers pour éviter que les résultats ne débordent. C'est presque toujours plus lent que d'utiliser simplement les FPU extrêmement performants des processeurs de bureau modernes. par exemple MAD, le décodeur mp3 à virgule fixe, est plus lent que libmpg123, et même s'il est de bonne qualité pour un décodeur à virgule fixe, libmpg123 a toujours moins d'erreur d'arrondi. wezm.net/technical/2008/04/mp3-decoder-libraries- comparé aux benchmarks sur un PPC G5.
Peter Cordes

Réponses:

35

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.

Dan
la source
8
Et sur certains processeurs, les mathématiques FP peuvent être plus rapides que les mathématiques entières. Le processeur Alpha avait une instruction de division FP mais pas une instruction entière, donc la division entière devait être effectuée dans le logiciel.
Gabe
SSEx accélérera-t-il également l'arithmétique à double précision? Je suis désolé, je ne connais pas trop bien SSE
Johannes Schaub - litb
1
@ JohannesSchaub-litb: SSE2 (baseline pour x86-64) a emballé double-precision FP. Avec seulement deux 64 bits doublepar registre, l'accélération potentielle est inférieure à celle floatdu code qui se vectorise bien. Scalaire floatet doubleutilise les registres XMM sur x86-64, avec l'ancien x87 utilisé uniquement pour long 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 octets xmm0..15au lieu de 8 -byte mm0..7, et les processeurs modernes ont un débit MMX pire que SSE.)
Peter Cordes
1
Mais les instructions de type entier MMX et SSE * / AVX2 sont en concurrence pour les mêmes unités d'exécution, donc utiliser les deux à la fois n'est presque jamais utile. Utilisez simplement les versions XMM / YMM plus larges pour faire plus de travail. L'utilisation simultanée d'un entier SIMD et de FP est en concurrence pour les mêmes registres, mais x86-64 en a 16. Mais les limites de débit total signifient que vous ne pouvez pas obtenir deux fois plus de travail en utilisant des unités d'exécution entières et FP en parallèle.
Peter Cordes
49

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 (tm) bicœur 32 bits 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 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:

#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
la source
8
pourquoi avez-vous mixé mult et div? Ne devrait-il pas être intéressant si mult est peut-être (ou comme prévu?) Beaucoup plus rapide que div?
Kyss Tao
13
La multiplication est beaucoup plus rapide que la division dans les cas entiers et flottants. Les performances de la division dépendent également de la taille des nombres. Je suppose généralement que la division est environ 15 fois plus lente.
Sogartar
4
pastebin.com/Kx8WGUfg J'ai pris votre repère et séparé chaque opération dans sa propre boucle et ajouté volatilepour m'en assurer. Sur Win64, le FPU est inutilisé et MSVC ne génère pas de code pour cela, il compile l' aide mulsset des divssinstructions XMM là - bas, qui sont 25x plus vite que la FPU dans Win32. La machine de test est Core i5 M 520 à 2,40 GHz
James Dunne
4
@JamesDunne soyez juste prudent, car les opérations fp vatteindront 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.
vladr
3
Ce "benchmark" n'a pas de parallélisme de données pour une exécution dans le désordre, car chaque opération est effectuée avec le même accumulateur ( v). Sur les récentes conceptions Intel, la division n'est pas du tout en pipeline ( divss/ divpsa une latence de 10 à 14 cycles et le même débit réciproque). mulsscependant, 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).
Peter Cordes
23

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é.

Ben Voigt
la source
4
+1 pour avoir souligné comment des benchmarks naïfs peuvent produire des boucles à temps 0 en raison d'opérations d'entiers constants déroulées. De plus, le compilateur peut supprimer complètement la boucle (entier ou FP) si le résultat n'est pas réellement utilisé.
vladr
La conclusion à cela est: il faut appeler une fonction ayant la variable de bouclage comme argument. Puisque je pense qu'aucun compilateur ne pourrait voir que la fonction ne fait rien et que l'appel peut être ignoré. Puisqu'il y a un surcoût d'appel, seules les différences de temps == (temps flottant - temps entier) seront significatives.
GameAlchemist
@GameAlchemist: De nombreux compilateurs éliminent les appels à des fonctions vides, comme effet secondaire de l'inlining. Vous devez faire un effort pour empêcher cela.
Ben Voigt
L'OP sonnait comme s'il parlait d'utiliser un entier pour des choses où FP serait un ajustement plus naturel, il faudrait donc plus de code entier pour obtenir le même résultat que le code FP. Dans ce cas, utilisez simplement FP. Par exemple, sur le matériel avec un FPU (par exemple un CPU de bureau), les décodeurs MP3 entiers à virgule fixe sont plus lents (et légèrement plus d'erreurs d'arrondi) que les décodeurs à virgule flottante. Les implémentations en virgule fixe des codecs existent principalement pour fonctionner sur des processeurs ARM dépouillés sans matériel FP, seulement FP émulé lentement.
Peter Cordes
un exemple pour le premier point: sur x86-64 avec AVX-512 il n'y a que 16 registres GP mais 32 registres zmm donc les maths scalaires en virgule flottante peuvent être plus rapides
phuclv
18

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.

Potatoswatter
la source
Ouais, ainsi que la conversion d'un entier rand en float dans la version à virgule flottante. Des idées sur une meilleure façon de tester cela?
maxpenguin
1
Si vous essayez de profiler la vitesse, regardez POSIX timespec_tou quelque chose de similaire. Enregistrez l'heure au début et à la fin de la boucle et faites la différence. Déplacez ensuite la randgé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.
Mike D.
3
@maxpenguin: la question est de savoir ce que vous testez. Artem a supposé que vous faisiez du graphisme, Carl s'est demandé si vous étiez sur une plate-forme intégrée sans FP, je suppose que vous codez la science pour un serveur. Vous ne pouvez pas généraliser ou «écrire» des benchmarks. Les repères sont échantillonnés à partir du travail réel effectué par votre programme. Une chose que je peux vous dire, c'est que cela ne restera pas "essentiellement la même vitesse" si vous touchez l'élément critique de performance dans votre programme, quel qu'il soit.
Potatoswatter
bon point et bonne réponse. 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 float 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 votre aide.
maxpenguin
18

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

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

Processeur Intel (R) Xeon (R) E5-2630L v2 (en cours d'exécution Trusty) DigitalOcean 1 Go Droplet

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 (tm) 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

Cela utilise le code de http://pastebin.com/Kx8WGUfg commebenchmark-pc.c

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

Graphique des données ci-dessus

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)
    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 (tm) 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
MrMesees
la source
gcc5 vectorise peut-être automatiquement quelque chose que gcc4.6 n'a pas fait? Est la benchmark-pcmesure 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).
Peter Cordes
Et BTW, pourquoi ne teste-t-il pas int, seulement shortet long? Sur Linux x86-64, shortest de 16 bits (et a donc des ralentissements de registre partiel dans certains cas), tandis que longet long longsont tous les deux de type 64 bits. (Peut-être est-il conçu pour Windows où x86-64 utilise toujours 32 bits long? Ou peut-être est-il conçu pour le mode 32 bits.) Sous Linux, l'ABI x32 a 32 bits longen mode 64 bits , donc si vous avez les bibliothèques installées , utilisez gcc -mx32pour compilateur pour ILP32. Ou utilisez simplement -m32et regardez les longchiffres.
Peter Cordes
Et vous devriez vraiment vérifier si votre compilateur a auto-vectorisé quoi que ce soit. Par exemple, en utilisant des addpsregistres xmm au lieu de addss, pour faire 4 FP ajoute en parallèle dans une instruction qui est aussi rapide que scalaire addss. (Utilisez -march=nativepour 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).
Peter Cordes
@cincodenada s'il vous plaît laissez les graphiques montrant les 15 complets sur le côté car ils illustrent alors la performance.
MrMesees
@PeterCordes je vais essayer de regarder demain, merci pour votre diligence.
MrMesees
7

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.

jcoder
la source
Il faut noter que sur Win64, les instructions FPU ne sont plus générées par le compilateur MSVC. La virgule flottante utilise toujours les instructions SIMD là-bas. Cela crée une grande différence de vitesse entre Win32 et Win64 concernant les flops.
James Dunne
5

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.

Goran D
la source
4

À 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

Artem Sokolov
la source
2
Les applications scientifiques utilisent FP. Le seul avantage de FP est que la précision est invariante d'échelle. C'est comme la notation scientifique. Si vous connaissez déjà l'échelle des nombres (par exemple, que la longueur de la ligne est un nombre de pixels), FP est évité. Mais avant d'arriver à tracer la ligne, ce n'est pas vrai.
Potatoswatter
4

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.

gnasher729
la source
Il y a beaucoup de spéculations sauvages ici, ne tenant compte d'aucun des effets secondaires présents dans le matériel, qui dominent souvent le temps de calcul. Ce n'est pas un mauvais point de départ, mais il doit être vérifié sur chaque application particulière via le profilage, et non pas enseigné comme évangile.
Ben Voigt
3

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:

  • court: 4.260s
  • int: 4.020s
  • long long: 3.350s
  • flotteur: 7.330s
  • double: 7,210 s
dan04
la source
1
Source, options de compilation et méthode de synchronisation? Je suis un peu surpris des résultats.
GManNickG
Même boucle que OP avec "rand ()% 365" remplacé par "1". Aucune optimisation. Heure utilisateur à partir de la commande "time".
dan04
13
"Aucune optimisation" est la clé. Vous ne profilez jamais avec l'optimisation désactivée, toujours profiler en mode "release".
Dean Harding
2
Dans ce cas, cependant, l'optimisation désactivée force l'opération à se produire, et est faite délibérément - la boucle est là pour dilater le temps à une échelle de mesure raisonnable. L'utilisation de la constante 1 supprime le coût de rand (). Un compilateur d'optimisation suffisamment intelligent verrait 1 ajouté 100000000 fois sans aucun moyen de sortir de la boucle et ajouterait simplement 100000000 en une seule opération. Cela permet de contourner le but, n'est-ce pas?
Stan Rogers
7
@Stan, rendez la variable volatile. Même un compilateur d'optimisation intelligent devrait alors honorer les multiples opérations.
vladr
0

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.

James Curran
la source
1
Veuillez envisager de regarder cela à nouveau en offrant plus qu'une opinion (d'autant plus que l'opinion semble aller à l'encontre des faits recueillis)
MrMesees
1
@MrMesees Bien que cette réponse ne soit pas très utile, je dirais qu'elle est cohérente avec les tests que vous avez effectués. Et les anecdotes historiques sont probablement bonnes aussi.
Jonatan Öström
En tant que personne ayant travaillé avec des 286 à l'époque, je peux confirmer; "Oui ils étaient!"
David H Parry