Performances des types intégrés: char vs court vs int vs float vs double

Cela peut sembler être une question un peu stupide mais vu la réponse d’ Alexandre C dans l’autre sujet, je suis curieux de savoir que s’il y a une différence de performance avec les types intégrés:

char vs short vs int vs float vs double .

Habituellement, nous ne considérons pas une telle différence de performance (le cas échéant) dans nos projets réels, mais j’aimerais le savoir à des fins éducatives. Les questions générales peuvent être posées est la suivante:

Flotteur vs. entier:

Historiquement, le virgule flottante peut être beaucoup plus lent que l’arithmétique entière. Sur les ordinateurs modernes, ce n’est plus vraiment le cas (il est un peu plus lent sur certaines plates-formes, mais à moins d’écrire du code parfait et d’optimiser pour chaque cycle, la différence sera comblée par les autres inefficacités de votre code).

Sur des processeurs assez limités, comme ceux des téléphones portables haut de gamme, le virgule flottante peut être un peu plus lent que le nombre entier, mais généralement dans un ordre de grandeur (ou mieux), tant qu’il existe un point flottant matériel. Il convient de noter que cet écart se réduit assez rapidement, car les téléphones cellulaires sont appelés à exécuter des charges de travail informatiques de plus en plus importantes.

Sur des processeurs très limités (téléphones cellulaires bon marché et votre grid-pain), il n’y a généralement pas de matériel en virgule flottante, de sorte que les opérations en virgule flottante doivent être émulées dans le logiciel. C’est lent – quelques ordres de grandeur plus lents que l’arithmétique entière.

Comme je l’ai déjà dit, les utilisateurs s’attendent à ce que leurs téléphones et autres appareils se comportent de plus en plus comme de «vrais ordinateurs», et les concepteurs de matériel renforcent rapidement les FPU pour répondre à cette demande. À moins que vous ne recherchiez chaque cycle précédent, ou que vous n’écriviez du code pour des processeurs très limités qui ne prennent en charge que peu ou pas de virgule flottante, la distinction entre les performances est sans importance pour vous.

Différents types d’entiers de taille:

En règle générale, les processeurs sont les plus rapides à opérer sur des entiers de leur taille de mot native (avec quelques réserves sur les systèmes 64 bits). Les opérations 32 bits sont souvent plus rapides que les opérations 8 ou 16 bits sur les processeurs modernes, mais cela varie beaucoup entre les architectures. De plus, rappelez-vous que vous ne pouvez pas considérer la vitesse d’un processeur de manière isolée. cela fait partie d’un système complexe. Même si le fonctionnement sur des nombres 16 bits est 2x plus lent que sur des nombres 32 bits, vous pouvez insérer deux fois plus de données dans la hiérarchie du cache lorsque vous le représentez avec des nombres de 16 bits au lieu de 32 bits. Si cela fait la différence entre le fait que toutes vos données proviennent du cache plutôt que de prendre de fréquents échecs de cache, l’access plus rapide à la mémoire l’emportera sur le fonctionnement plus lent du processeur.

Autres notes:

La vectorisation fait pencher la balance davantage en faveur de types plus étroits (nombres entiers et entiers de 8 et 16 bits) – vous pouvez effectuer plus d’opérations dans un vecteur de même largeur. Cependant, un bon code vectoriel est difficile à écrire, ce n’est donc pas comme si vous aviez cet avantage sans un travail minutieux.

Pourquoi y a-t-il des différences de performance?

Il n’y a vraiment que deux facteurs qui déterminent si une opération est rapide ou non sur une CPU: la complexité du circuit de l’opération et la demande de l’utilisateur pour que l’opération soit rapide.

(Dans des limites raisonnables) toute opération peut être effectuée rapidement si les concepteurs de puces sont prêts à lancer suffisamment de transistors sur le problème. Mais les transistors coûtent cher (ou plutôt, utiliser beaucoup de transistors pour agrandir votre puce, ce qui signifie que vous obtenez moins de puces par tranche et des rendements inférieurs, ce qui coûte cher). ils le font en fonction de la demande des utilisateurs (perçue). En gros, vous pouvez penser à diviser les opérations en quatre catégories:

  high demand low demand high complexity FP add, multiply division low complexity integer add popcount, hcf boolean ops, shifts 

Les opérations à forte demande et à faible complexité seront rapides pour presque tous les processeurs: ce sont les avantages les plus faciles à obtenir et confèrent un bénéfice maximal à chaque utilisateur.

les opérations à forte demande et à haute complexité seront rapides sur les processeurs coûteux (comme ceux utilisés dans les ordinateurs), car les utilisateurs sont prêts à payer pour ces processeurs. Vous n’êtes probablement pas prêt à payer 3 $ de plus pour que votre grid-pain ait une multiplication rapide de la FP, cependant, les CPU bon marché lésineront sur ces instructions.

les opérations à faible demande et à complexité élevée seront généralement lentes sur presque tous les processeurs; il n’y a tout simplement pas assez d’avantages pour justifier le coût.

les opérations à faible demande et à faible complexité seront rapides si quelqu’un s’y méfie et si elles n’existent pas autrement.

Lectures complémentaires:

  • Agner Fog maintient un bon site Web avec beaucoup de discussions sur les détails de performance de bas niveau (et a une méthodologie de collecte de données très scientifique pour le sauvegarder).
  • Le Manuel de référence de l’optimisation des architectures Intel® 64 et IA-32 (lien de téléchargement PDF en bas de la page) couvre également un grand nombre de ces problèmes, même s’il se concentre sur une famille d’architectures spécifique.

Absolument.

Tout d’abord, bien sûr, cela dépend entièrement de l’architecture du processeur en question.

Cependant, les types intégraux et à virgule flottante sont traités très différemment, de sorte que ce qui suit est presque toujours le cas:

  • pour les opérations simples, les types intégraux sont rapides . Par exemple, l’addition d’entiers n’a souvent qu’une latence de cycle unique et la multiplication d’entiers se situe généralement autour de 2 à 4 cycles, IIRC.
  • Les types à virgule flottante fonctionnaient beaucoup plus lentement. Sur les processeurs actuels, cependant, leur débit est excellent et chaque unité en virgule flottante peut généralement supprimer une opération par cycle, ce qui conduit à un débit identique (ou similaire) à celui des opérations sur nombres entiers. Cependant, la latence est généralement pire. L’addition en virgule flottante a souvent une latence autour de 4 cycles (vs 1 pour ints).
  • pour certaines opérations complexes, la situation est différente, voire inversée. Par exemple, la division sur FP peut avoir moins de latence que pour les entiers, tout simplement parce que l’opération est complexe à mettre en œuvre dans les deux cas, mais plus utile sur les valeurs de FP.

Sur certains processeurs, les doubles peuvent être nettement plus lents que les flottants. Sur certaines architectures, il n’y a pas de matériel dédié pour les doubles, et elles sont donc traitées en faisant passer deux blocs de taille flottante, ce qui vous donne un débit plus faible et deux fois plus de latence. Sur les autres (la FPU x86, par exemple), les deux types sont convertis au même format en virgule flottante au format 80 bits, dans le cas de x86), si bien que les performances sont identiques. D’autres encore, float et double ont un support matériel approprié, mais comme float a moins de bits, il peut être fait un peu plus rapidement, réduisant généralement un peu la latence par rapport aux opérations doubles.

Disclaimer: tous les timings et caractéristiques mentionnés sont simplement extraits de la mémoire. Je n’ai rien regardé, donc ça peut être faux. 😉

Pour différents types d’entiers, la réponse varie énormément selon l’architecture du processeur. L’architecture x86, en raison de sa longue histoire complexe, doit prendre en charge les opérations sur bits 8, 16, 32 (et 64 actuelles) en natif, et en général, elles sont toutes aussi rapides (elles utilisent essentiellement le même matériel et zéro). sortir les bits supérieurs si nécessaire).

Cependant, sur d’autres processeurs, les types de données plus petits qu’un int peuvent être plus coûteux à charger / à stocker (l’écriture d’un octet dans la mémoire peut être effectuée en chargeant l’intégralité du mot de 32 bits le seul octet dans un registre, puis écrivez le mot entier en arrière). De même, pour les types de données plus grands que int , certaines CPU peuvent devoir diviser l’opération en deux, charger / stocker / calculer les moitiés inférieure et supérieure séparément.

Mais sur x86, la réponse est que cela n’a pas d’importance. Pour des raisons historiques, le processeur doit avoir un support assez robuste pour chaque type de données. Donc, la seule différence que vous remarquerez probablement est que les opérations en virgule flottante ont plus de latence (mais un débit similaire, elles ne sont donc pas plus lentes , du moins si vous écrivez correctement votre code).

Je ne pense pas que quiconque ait mentionné les règles de promotion des entiers. En standard C / C ++, aucune opération ne peut être effectuée sur un type plus petit que int . Si char ou short sont plus petits que int sur la plate-forme actuelle, ils sont implicitement promus dans int (qui est une source majeure de bogues). Le complicateur est nécessaire pour faire cette promotion implicite, il n’ya aucun moyen de contourner cela sans enfreindre la norme.

Les promotions entières signifient qu’aucune opération (addition, bit à bit, logique, etc.) dans le langage ne peut se produire sur un type entier plus petit que int. Ainsi, les opérations sur char / short / int sont généralement aussi rapides que les précédentes.

Et en plus des promotions sur les entiers, il y a les “conversions arithmétiques habituelles”, ce qui signifie que C s’efforce de rendre les deux opérandes du même type, en convertissant l’une des deux, si elles sont différentes.

Cependant, le processeur peut effectuer diverses opérations de chargement / stockage au niveau 8, 16, 32, etc. Sur les architectures 8 et 16 bits, cela signifie souvent que les types 8 et 16 bits sont plus rapides malgré les promotions en nombres entiers. Sur un processeur 32 bits, cela peut en fait signifier que les plus petits types sont plus lents , car ils veulent que tout soit parfaitement aligné sur des blocs de 32 bits. Les compilateurs 32 bits optimisent généralement la vitesse et allouent des types d’entiers plus petits dans un espace plus grand que celui spécifié.

Bien que les types de nombres entiers plus petits prennent généralement moins de place que les plus grands, si vous avez l’intention d’optimiser la taille de la RAM, ils préfèrent.

Y a-t-il une différence de performance entre l’arithmétique intégrale et l’arithmétique à virgule flottante?

Oui. Cependant, ceci est très spécifique à la plate-forme et au processeur. Différentes plates-formes peuvent effectuer différentes opérations arithmétiques à différentes vitesses.

Cela étant dit, la réponse en question était un peu plus précise. pow() est une routine polyvalente qui fonctionne sur des valeurs doubles. En lui donnant des valeurs entières, cela fait toujours tout le travail nécessaire pour gérer les exposants non entiers. L’utilisation de la multiplication directe contourne une grande partie de la complexité, c’est-à-dire là où la vitesse entre en jeu. Ce n’est vraiment pas un problème (tellement) de différents types, mais plutôt de contourner une grande quantité de code complexe nécessaire pour faire fonctionner le pow avec n’importe quel exposant.

Dépend de la composition du processeur et de la plate-forme.

Les plates-formes dotées d’un coprocesseur à virgule flottante peuvent être plus lentes que l’arithmétique intégrale, car des valeurs doivent être transférées vers et depuis le coprocesseur.

Si le traitement en virgule flottante est au cœur du processeur, le temps d’exécution peut être négligeable.

Si les calculs en virgule flottante sont émulés par le logiciel, alors l’arithmétique intégrale sera plus rapide.

En cas de doute, profil.

Obtenez la programmation fonctionnant correctement et robuste avant d’optimiser.

La première réponse ci-dessus est géniale et j’ai copié un petit bloc dans le duplicata suivant (car c’est là que j’ai fini en premier).

Est-ce que “char” et “small int” sont plus lents que “int”?

Je voudrais proposer le code suivant, qui alloue, initialise et effectue des calculs arithmétiques sur les différentes tailles d’entiers:

 #include  #include  using std::cout; using std::cin; using std::endl; LARGE_INTEGER StartingTime, EndingTime, ElapsedMicroseconds; LARGE_INTEGER Frequency; void inline showElapsed(const char activity []) { QueryPerformanceCounter(&EndingTime); ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart; ElapsedMicroseconds.QuadPart *= 1000000; ElapsedMicroseconds.QuadPart /= Frequency.QuadPart; cout << activity << " took: " << ElapsedMicroseconds.QuadPart << "us" << endl; } int main() { cout << "Hallo!" << endl << endl; QueryPerformanceFrequency(&Frequency); const int32_t count = 1100100; char activity[200]; //-----------------------------------------------------------------------------------------// sprintf_s(activity, "Initialise & Set %d 8 bit integers", count); QueryPerformanceCounter(&StartingTime); int8_t *data8 = new int8_t[count]; for (int i = 0; i < count; i++) { data8[i] = i; } showElapsed(activity); sprintf_s(activity, "Add 5 to %d 8 bit integers", count); QueryPerformanceCounter(&StartingTime); for (int i = 0; i < count; i++) { data8[i] = i + 5; } showElapsed(activity); cout << endl; //-----------------------------------------------------------------------------------------// //-----------------------------------------------------------------------------------------// sprintf_s(activity, "Initialise & Set %d 16 bit integers", count); QueryPerformanceCounter(&StartingTime); int16_t *data16 = new int16_t[count]; for (int i = 0; i < count; i++) { data16[i] = i; } showElapsed(activity); sprintf_s(activity, "Add 5 to %d 16 bit integers", count); QueryPerformanceCounter(&StartingTime); for (int i = 0; i < count; i++) { data16[i] = i + 5; } showElapsed(activity); cout << endl; //-----------------------------------------------------------------------------------------// //-----------------------------------------------------------------------------------------// sprintf_s(activity, "Initialise & Set %d 32 bit integers", count); QueryPerformanceCounter(&StartingTime); int32_t *data32 = new int32_t[count]; for (int i = 0; i < count; i++) { data32[i] = i; } showElapsed(activity); sprintf_s(activity, "Add 5 to %d 32 bit integers", count); QueryPerformanceCounter(&StartingTime); for (int i = 0; i < count; i++) { data32[i] = i + 5; } showElapsed(activity); cout << endl; //-----------------------------------------------------------------------------------------// //-----------------------------------------------------------------------------------------// sprintf_s(activity, "Initialise & Set %d 64 bit integers", count); QueryPerformanceCounter(&StartingTime); int64_t *data64 = new int64_t[count]; for (int i = 0; i < count; i++) { data64[i] = i; } showElapsed(activity); sprintf_s(activity, "Add 5 to %d 64 bit integers", count); QueryPerformanceCounter(&StartingTime); for (int i = 0; i < count; i++) { data64[i] = i + 5; } showElapsed(activity); cout << endl; //-----------------------------------------------------------------------------------------// getchar(); } /* My results on i7 4790k: Initialise & Set 1100100 8 bit integers took: 444us Add 5 to 1100100 8 bit integers took: 358us Initialise & Set 1100100 16 bit integers took: 666us Add 5 to 1100100 16 bit integers took: 359us Initialise & Set 1100100 32 bit integers took: 870us Add 5 to 1100100 32 bit integers took: 276us Initialise & Set 1100100 64 bit integers took: 2201us Add 5 to 1100100 64 bit integers took: 659us */ 

Mes résultats dans MSVC sur i7 4790k:

Initialise & Set 1100100 Entiers 8 bits pris: 444us
Ajouter 5 à 1100100 entiers 8 bits a pris: 358us

Initialise & Set 1100100 Entiers 16 bits pris: 666us
Ajouter 5 à 1100100 entiers 16 bits ont pris: 359us

Initialise & Set 1100100 Entiers 32 bits pris: 870us
Ajouter 5 à 1100100 entiers 32 bits ont pris: 276us

Initialise & Set 1100100 Entiers 64 bits pris: 2201us
Ajouter 5 à 1100100 entiers 64 bits ont pris: 659us

Non, pas vraiment. Cela dépend bien sûr du processeur et du compilateur, mais la différence de performance est généralement négligeable, voire inexistante.

Il y a certainement une différence entre l’arithmétique en virgule flottante et en arithmétique entière. Selon le matériel et les micro-instructions spécifiques au processeur, vous obtenez des performances et / ou une précision différentes. Bon google termes pour les descriptions précises (je ne sais pas exactement non plus):

FPU x87 MMX SSE

En ce qui concerne la taille des entiers, il est préférable d’utiliser la taille de mot de la plate-forme / architecture (ou le double), ce qui revient à un int32_t sur x86 et à int64_t sur x86_64. Les processeurs SOme peuvent avoir des instructions insortingnsèques qui gèrent plusieurs de ces valeurs à la fois (comme SSE (virgule flottante) et MMX), ce qui accélérera les ajouts ou les multiplications parallèles.

En règle générale, les mathématiques entières sont plus rapides que les mathématiques à virgule flottante. C’est parce que les calculs entiers impliquent des calculs plus simples. Cependant, dans la plupart des opérations, on parle de moins d’une douzaine d’horloges. Pas de millis, de micros, de nanos ou de tiques; les horloges. Ceux qui se produisent entre 2-3 milliards de fois par seconde dans les cœurs modernes. De plus, depuis le 486, de nombreux cœurs ont un ensemble d’unités de traitement à virgule flottante ou de FPU, qui sont câblées pour effectuer l’arithmétique à virgule flottante efficacement, et souvent en parallèle avec le processeur.

En conséquence, bien que ce soit techniquement plus lent, les calculs en virgule flottante sont encore si rapides que toute tentative de chronométrer la différence comporterait plus d’erreurs inhérentes au mécanisme de synchronisation et à la planification des threads que pour effectuer le calcul. Utilisez ints quand vous le pouvez, mais comprenez quand vous ne pouvez pas, et ne vous inquiétez pas trop de la vitesse de calcul relative.