La Performance de types intégrés: char vs court vs int ou float vs double
cette question peut sembler un peu stupide, mais en voyant Alexandre C répondre dans l'autre sujet, je suis curieux de savoir s'il y a une différence de performance avec les types intégrés:
char
vsshort
vsint
vsfloat
vs.double
.
Habituellement, nous ne considérons pas une telle différence de performance (s'il y en a) dans nos projets de la vie réelle, mais je voudrais savoir ce pour des fins éducatives. Les questions générales peuvent être posées est:
-
y a-t-il une différence de rendement entre l'arithmétique intégrale et l'arithmétique à virgule flottante?
-
qui est le plus rapide? Quelle est la raison d'être plus rapide? Veuillez expliquer ceci.
9 réponses
Float vs entier:
historiquement, la virgule flottante pourrait être beaucoup plus lente que l'arithmétique entière. Sur les ordinateurs modernes, ce n'est plus vraiment le cas (c'est un peu plus lent sur certaines plateformes, mais à moins d'écrire du code parfait et d'optimiser pour chaque cycle, la différence sera submergée par les autres inefficacités de votre code).
sur des processeurs un peu limités, comme ceux des téléphones cellulaires haut de gamme, floating-point peut être un peu plus lent que l'entier, mais il est généralement dans un ordre de grandeur (ou mieux), tant qu'il y a matériel floating-point disponible. Il est intéressant de noter que cet écart se rétrécit assez rapidement, car les téléphones cellulaires sont appelés à exécuter de plus en plus de charges de travail informatiques générales.
sur très processeurs limités (téléphones cellulaires bon marché et votre grille-pain), il n'y a généralement pas de matériel flottant, donc flottant-point les opérations doivent être émulées dans le logiciel. C'est lent -- quelques ordres de grandeur plus lent que l'arithmétique entière.
comme je l'ai dit cependant, les gens 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 se renforcent rapidement pour répondre à cette demande. Sauf si vous courez après chaque cycle, ou si vous écrivez du code pour des cpu très limités qui ont peu ou pas de support flottant, la distinction performance n'est pas question pour vous.
de taille Différente types d'entiers:
typiquement, CPUs sont les plus rapides à opérer sur des entiers de leur taille de mot natif (avec quelques mises en garde 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 CPU modernes, mais cela varie un peu entre les architectures. En outre, rappelez-vous que vous ne pouvez pas considérer la vitesse d'un CPU dans l'isolement; il fait partie d'un système complexe. Même si travailler sur des nombres 16 bits est 2 fois plus lent que travailler 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 16 bits au lieu de 32 bits. Si cela fait la différence entre avoir toutes vos données provenant du cache au lieu de prendre des erreurs fréquentes de cache, alors l'accès plus rapide à la mémoire supplantera le fonctionnement plus lent du CPU.
autres notes:
La vectorisation fait pencher la balance en faveur des types plus étroits ( float
et des entiers de 8 et 16 bits) -- vous pouvez effectuer plus d'opérations dans un vecteur de la même largeur. Cependant, un bon code vectoriel est difficile à écrire, donc ce n'est pas comme si vous obtenez cet avantage sans beaucoup de travail minutieux.
Pourquoi y a-t-il des différences de rendement?
il n'y a vraiment que deux facteurs qui influencent si une opération est ou non rapide sur un CPU: la complexité du circuit de l'opération, et la demande de l'utilisateur pour l'opération d'être rapide.
(dans la limite du raisonnable) n'importe quelle opération peut être faite rapidement, si les concepteurs de puce sont disposés à jeter assez de transistors au problème. Mais les transistors coûtent de l'argent (ou plutôt, en utilisant beaucoup de transistors rend votre puce plus grande, ce qui signifie que vous obtenez moins de puces par plaquette et des rendements plus faibles, ce qui coûte de l'argent), de sorte que les concepteurs de puces doivent équilibrer combien de complexité à utiliser pour et ils le font en fonction de la demande (perçue) des utilisateurs. En gros, vous pourriez 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
haute demande, opérations de faible complexité sera rapide sur presque n'importe quel CPU: ils sont le fruit bas-accrocheur, et de conférer un maximum d'avantage utilisateur par transistor.
les opérations à forte demande et à haute complexité seront rapides sur les CPU coûteux (comme ceux utilisés dans les ordinateurs), parce que les utilisateurs sont prêts à payer pour eux. Vous n'êtes probablement pas prêt à payer un supplément de 3 $pour votre grille-pain pour avoir un rapide FP multiplier, cependant, donc CPUs bon marché sera lésiner sur ces instructions.
les opérations à faible demande et très complexes seront généralement lentes sur presque tous les transformateurs; 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 se donne la peine d'y penser, et inexistantes autrement.
autre lecture:
- Agner Fog maintient un beau site web avec beaucoup de discussion des détails de performance de bas niveau (et a la méthodologie de collecte de données très scientifique pour le soutenir).
- le manuel de référence D'optimisation des Architectures Intel® 64 et IA-32 (lien de téléchargement PDF en bas de la page) couvre un grand nombre de ces questions, bien qu'il soit centrée sur une famille d'architectures.
absolument.
tout d'abord, bien sûr, cela dépend entièrement de l'architecture CPU en question.
cependant, les types à virgule intégrale 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 fast . Par exemple, l'addition d'entier a souvent seulement la latence d'un cycle simple, et la multiplication d'entier est typiquement environ 2 à 4 cycles, IIRC. Les types à virgule flottante
- utilisés pour effectuer beaucoup plus lent. Sur les CPU d'aujourd'hui, cependant, ils ont un excellent débit, et une Chaque unité de virgule flottante peut habituellement retirer une opération par cycle, conduisant à la même (ou similaire) débit que pour les opérations entières. Cependant, la latence est généralement pire. L'addition en virgule flottante a souvent une latence autour de 4 cycles (vs 1 pour les ints).
- pour certaines opérations complexes, la situation est différent, ou même inversé. Par exemple, la division sur FP peut avoir moins de latence que pour les entiers, simplement parce que l'opération est complexe à mettre en œuvre dans les deux cas, mais il est plus souvent utile sur les valeurs FP, donc plus d'effort (et transistors) peut être dépensé en optimisant ce cas.
sur certains CPU, les doubles peuvent être significativement plus lents que les flotteurs. Sur certaines architectures, il n'y a pas de matériel dédié pour les doubles, manipulé en passant deux morceaux de la taille d'un flotteur à travers, vous donnant un pire débit et deux fois la latence. Sur d'autres (le x86 FPU, par exemple), les deux types sont convertis au même format interne 80-bit floating point, dans le cas de x86), donc la performance est identique. Sur d'autres encore, à la fois float et double ont un support matériel adéquat, mais parce que float a moins de bits, il peut être fait un peu plus rapidement, réduisant typiquement la latence un peu par rapport aux opérations doubles.
Responsabilité: Tous les temps mentionnés et les caractéristiques sont juste tirés de la mémoire. Je n'ai pas l'air de tout ça, de sorte qu'il peut être mauvais. ;)
pour différents types d'entiers, la réponse varie énormément en fonction de l'architecture CPU. L'architecture x86, en raison de sa longue histoire alambiquée, doit supporter les opérations 8, 16, 32 (et aujourd'hui 64) bits nativement, et en général, ils sont tous aussi rapides ( ils utilisent essentiellement le même matériel, et juste zéro sur les bits supérieurs que nécessaire.)
cependant, sur D'autres CPU, les types de données plus petits qu'un int
peuvent être plus coûteux à charger/stocker (écrire un octet en mémoire pourrait devoir être fait en chargeant le mot entier de 32 bits dans lequel il est situé, et puis faire le masquage de bit pour mettre à jour le octet simple dans un registre, et puis écrire le mot entier retour). De même, pour les types de données plus grands que int
, certains CPU peuvent avoir à diviser l'opération en deux, chargement / stockage/calcul des moitiés inférieure et supérieure séparément.
mais sur x86, la réponse est que la plupart du temps ça n'a pas d'importance. Pour des raisons historiques, le CPU doit avoir un support assez robuste pour chaque type de données. Donc la seule différence que vous êtes susceptible de remarquer est que les ops à virgule flottante ont plus de latence (mais un débit similaire, donc ils ne sont pas plus lent en soi, au moins si vous écrivez votre code correctement)
Je ne pense pas que quelqu'un 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 à int (qui est une source majeure de bogues). Le compilateur est tenu de faire cette promotion implicite, il n'y a aucun moyen de l'contourner sans violer la norme.
les promotions integer signifient qu'aucune opération (addition, bitwise, logical etc etc) dans la langue 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 premières sont promues sur char / short / int.
et en plus des promotions integer, il y a les" conversions arithmétiques habituelles", ce qui signifie que C s'efforce de faire les deux opérandes du même type, en convertissant l'un d'eux au plus grand des deux, s'ils sont différents.
cependant, le CPU peut effectuer diverses opérations de chargement / stockage sur 8, 16, 32 etc niveau. Sur les architectures 8 et 16 bits, cela signifie souvent que les types 8 et 16 bits sont plus rapides malgré les promotions integer. Sur un CPU 32 bits, cela signifie Peut-être que les petits types sont plus lents , parce qu'il veut que tout soit parfaitement aligné en morceaux 32 bits. Les compilateurs 32 bits optimisent généralement pour la vitesse et attribuent des types entiers plus petits dans l'espace plus grand que spécifié.
Though généralement les petits types entiers prennent bien sûr moins d'espace que les plus grands, donc si vous avez l'intention d'optimiser pour la taille de la RAM, ils sont à préférer.
y a-t-il une différence de rendement entre l'arithmétique intégrale et l'arithmétique à virgule flottante?
Oui. Cependant, ceci est très spécifique à la plateforme et au CPU. Différentes plateformes peuvent effectuer différentes opérations arithmétiques à différentes vitesses.
cela dit, la réponse en question était un peu plus précise. pow()
est une routine à usage général qui fonctionne sur des valeurs doubles. En le nourrissant entier valeurs, il fait toujours tout le travail qui serait nécessaire pour manipuler les exposants non-entiers. L'utilisation de la multiplication directe contourne une grande partie de la complexité, qui est 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 requis pour faire fonction pow avec n'importe quel exposant.
Dépend de la composition du processeur et de la plate-forme.
les plates-formes qui ont un coprocesseur à virgule flottante peuvent être plus lentes que l'arithmétique intégrale en raison du fait que les 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 un logiciel, alors l'arithmétique intégrale sera plus rapide.
dans le doute, profil.
obtenir la programmation fonctionne correctement et robuste avant d'optimiser.
La première réponse ci-dessus est grande et j'ai copié un petit bloc de travers à la suite double (car c'est là que j'ai fini premier).
est-ce que "char" et "small int"sont plus lents que "int"?
j'aimerais vous proposer le code suivant qui permet d'affecter, d'initialiser et de faire de l'arithmétique sur les différentes tailles d'entiers:
#include <iostream>
#include <windows.h>
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 en MSVC sur i7 4790k:
Initialise & Set 1100100 8 bits entiers pris: 444us
Ajouter 5 à 1100100 8 bits entiers pris: 358us
Initialise & Set 1100100 entiers 16 bits pris: 666us
Ajouter 5 à 1100100 entiers de 16 bits pris: 359us
Initialise & Set 1100100 32 bits entiers pris: 870us
Ajouter 5 à 1100100 entiers 32 bits pris: 276us
Initialise & Set 1100100 64 bit entiers pris: 2201us
Ajouter 5 à 1100100 64 bits entiers pris: 659us
non, pas vraiment. Cela dépend bien sûr du CPU et du compilateur, mais la différence de performance est généralement négligeable - s'il y en a même.
il y a certainement une différence entre l'arithmétique flottante et l'arithmétique entière. Selon le matériel spécifique du CPU et les micro-instructions, vous obtenez des performances et/ou une précision différentes. Bons termes google pour les descriptions précises (Je ne sais pas non plus exactement):
FPU x87 MMX SSE
en ce qui concerne la taille des entiers, il est préférable d'utiliser la plateforme / architecture word size (ou le double que), ce qui revient à un int32_t
sur x86 et int64_t
sur x86_64. Certains processeurs peuvent avoir des instructions intrinsèques qui traitent plusieurs de ces valeurs à la fois (comme SSE (floating point) et MMX), ce qui accélérera les additions ou les multiplications parallèles.
généralement, les mathématiques entières sont plus rapides que les mathématiques à virgule flottante. C'est parce que les maths entières impliquent des calculs plus simples. Cependant, dans la plupart des opérations que nous parlons moins d'une douzaine d'horloges. Pas des millimètres, des micros, des nanos ou des tiques; des horloges. Ceux qui se produisent entre 2-3 milliards de fois par seconde dans les noyaux modernes. En outre, depuis le 486 beaucoup de noyaux ont un ensemble D'Unités de traitement flottantes ou FPUs, qui sont câblés dur pour effectuer l'arithmétique flottante efficacement, et souvent en parallèle avec le CPU.
en raison de ces, bien que techniquement, il est plus lent, les calculs à virgule flottante sont encore si rapide que toute tentative de chronométrer la différence aurait plus d'erreur inhérente dans le mécanisme de synchronisation et la programmation de thread qu'il ne faut réellement pour effectuer le calcul. Utilisez les ints quand vous le pouvez, mais comprenez quand vous ne le pouvez pas, et ne vous inquiétez pas trop au sujet de la vitesse de calcul relative.