web-dev-qa-db-fra.com

Performance des types intégrés: char vs short vs int vs float vs double

Cela peut sembler être une question un peu stupide, mais vu le reply d'Alexandre C dans l'autre sujet, je suis curieux de savoir que s'il existe une différence de performances 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 pouvant être posées sont les suivantes:

  • Existe-t-il une différence de performance entre l'arithmétique intégrale et l'arithmétique à virgule flottante?

  • Lequel est plus vite? Quelle est la raison d'être plus rapide? S'il vous plaît expliquer cela.

60
Nawaz

Float vs. entier:

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

Sur les processeurs quelque peu limités, comme ceux des téléphones cellulaires haut de gamme, la virgule flottante peut être un peu plus lente que l'entier, mais elle est généralement d'un ordre de grandeur (ou mieux), tant que la virgule flottante est disponible. Il convient de noter que cet écart se résorbe assez rapidement, car les téléphones portables sont appelés à exécuter des charges de travail informatiques de plus en plus générales.

Sur _ {très} processeurs limités (téléphones portables bon marché et votre grille-pain), il n'y a généralement pas de matériel à virgule flottante; les opérations en virgule flottante doivent donc ê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 renforcent rapidement les FPU pour répondre à cette demande. À moins que vous ne couriez après chaque dernier cycle ou que vous é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 performances ne compte pas pour vous.

Types entiers de tailles différentes:

En règle générale, CPU sont les plus rapides à fonctionner sur des entiers de leur taille native (avec quelques réserves concernant 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 beaucoup d'une architecture à l'autre. 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 celui sur des nombres 32 bits, vous pouvez insérer deux fois plus de données dans la hiérarchie du cache lorsque vous les représentez avec des nombres 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 des erreurs de cache fréquentes, l'accès plus rapide à la mémoire l'emportera sur le fonctionnement plus lent de la CPU.

Autres notes:

La vectorisation fait pencher la balance plus loin en faveur des types plus étroits (float et entiers 8 et 16 bits) - vous pouvez effectuer davantage d'opérations dans un vecteur de même largeur. Cependant, il est difficile d’écrire un bon code vectoriel. Ce n’est donc pas comme si vous bénéficiez de cet avantage sans un travail minutieux.

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

En réalité, seuls deux facteurs 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 disposés à jeter suffisamment de transistors au problème. Mais les transistors coûtent de l'argent (ou plutôt, utiliser un grand nombre de transistors augmente la taille de votre puce, ce qui signifie que vous obtenez moins de puces par tranche et moins de rendements, ce qui coûte de l'argent). ils le font en fonction de la demande (perçue) des utilisateurs. 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 à faible demande et à faible complexité seront rapides sur presque tous les processeurs: ce sont les fruits les plus faciles à gagner et confèrent un bénéfice utilisateur maximum par transistor.

les opérations très demandées et complexes seront rapides sur des processeurs coûteux (comme ceux utilisés dans les ordinateurs), car les utilisateurs sont disposés à les payer. Vous n'êtes probablement pas disposé à payer 3 $ de plus pour que votre grille-pain ait une multiplication rapide FP, cependant, de sorte que les processeurs bon marché lésineront sur ces instructions.

les opérations à faible demande et complexes 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 ne veut pas y penser, voire inexistantes.

Lectures supplémentaires:

  • Agner Fog gère un site Web Nice / avec beaucoup de discussions sur les performances de bas niveau (et dispose d'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 (le lien de téléchargement PDF est en bas de la page) couvre également bon nombre de ces problèmes, bien qu'il se concentre sur une famille d'architectures spécifique.
108
Stephen Canon

Absolument.

Tout d’abord, bien sûr, cela dépend entièrement de l’architecture de la CPU 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 fast. Par exemple, l’addition d’entiers n’a souvent qu’une latence d’un cycle et la multiplication d’entiers se situe généralement entre 2 et 4 cycles (IIRC).
  • Les types à virgule flottante sont utilisés beaucoup plus lentement. Sur les processeurs actuels, toutefois, leur débit est excellent et chaque unité à virgule flottante peut généralement abandonner une opération par cycle, ce qui permet d'obtenir 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 d'environ 4 cycles (vs 1 pour les ints).
  • pour certaines opérations complexes, la situation est différente, voire inversée. Par exemple, la division sur FP peut avoir une latence less inférieure à celle des nombres entiers, tout simplement parce que l'opération est complexe à mettre en œuvre dans les deux cas, mais elle est généralement plus utile sur les valeurs FP. des efforts (et des transistors) peuvent être consacrés à l'optimisation de ce cas.

Sur certains processeurs, les doublons peuvent être nettement plus lents que les flottants. Sur certaines architectures, il n'y a pas de matériel dédié aux doublons, et ils sont donc gérés en faisant passer deux fragments de taille flottante, ce qui vous donne un débit pire et deux fois plus de temps de latence. Sur d'autres (FPU x86, par exemple), les deux types sont convertis au même format interne à virgule flottante 80 bits, dans le cas de x86), de sorte que les performances sont identiques. Sur d’autres encore, float et double prennent en charge le matériel adéquat, mais comme float contient moins de bits, cela peut être fait un peu plus vite, ce qui réduit généralement un peu le temps de latence par rapport aux opérations doubles.

Clause de non-responsabilité: tous les timings et caractéristiques mentionnés sont simplement extraits de la mémoire. Je n'ai rien cherché, alors c'est peut-être faux. ;)

Pour différents types d'entiers, la réponse varie énormément selon l'architecture du processeur. En raison de sa longue histoire, l’architecture x86 doit prendre en charge les opérations de 8, 16, 32 (et aujourd’hui 64 bits) en mode natif. En général, elles sont toutes aussi rapides (elles utilisent essentiellement le même matériel et les bits supérieurs au besoin).

Cependant, sur d'autres processeurs, les types de données plus petits qu'une int peuvent être plus coûteux à charger/stocker (l'écriture d'un octet dans la mémoire peut être effectuée en chargeant le mot 32 bits entier dans lequel il se trouve, puis masquer l’octet unique dans un registre, puis écrivez le mot entier). De même, pour les types de données supérieurs à int, il est possible que certaines CPU doivent scinder 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 disposer d'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 ralenties en soi, du moins si vous écrivez votre code correctement)

9
jalf

Je ne pense pas que quiconque ait mentionné les règles de promotion des nombres entiers. En C/C++ standard, aucune opération ne peut être effectuée sur un type inférieur à int. Si char ou short est plus petit que int sur la plate-forme actuelle, ils sont implicitement promus en int (source majeure de bogues). Le compliant est tenu de faire cette promotion implicite, il n’ya pas moyen de la contourner sans violer la norme.

Les promotions entières signifient qu'aucune opération (addition, bit à bit, logique, etc.) dans la langue 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, les premières étant promues au second.

Et en plus des promotions sur les nombres 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'un d'eux en le plus grand, s'ils sont différents.

Cependant, la CPU 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 d’entier. Sur un processeur 32 bits, cela peut en fait signifier que les types plus petits sont plus lents , car il veut que tout soit parfaitement aligné en morceaux de 32 bits. Les compilateurs 32 bits optimisent généralement la vitesse et allouent des types entiers plus petits dans un espace plus grand que celui spécifié.

Bien que généralement les types de plus petit nombre entiers de cours prennent moins d’espace que les plus grands, donc si vous souhaitez optimiser la taille RAM, ils sont préférables.

6
Lundin

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

Oui. Cependant, cela dépend beaucoup de la plate-forme et du 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 à usage général qui fonctionne sur des valeurs doubles. En lui fournissant des valeurs entières, il effectue tout le travail nécessaire pour gérer les exposants non entiers. L’utilisation de la multiplication directe permet d’éviter beaucoup de complexité, c’est là que la vitesse entre en jeu. Ce n'est vraiment pas un problème (tellement) de types différents, mais plutôt de contourner une grande quantité de code complexe nécessaire pour faire fonctionner pow avec un exposant.

2
Reed Copsey

La première réponse ci-dessus est excellente et j'en ai copié un petit bloc dans la copie suivante (car c'est là que j'ai fini en premier).

Les caractères "char" et "small int" sont-ils plus lents que "int"?

Je voudrais offrir le code suivant, qui définit les profils d’affectation, d’initialisation et d’arithmétique des différentes tailles d’entier

#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 Entiers 8 bits pris: 444us
Ajouter 5 à 1100100 entiers 8 bits pris: 358us

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

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

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

2
Researcher

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 en raison du fait que les valeurs doivent être transférées vers et à partir du coprocesseur. 

Si le traitement en virgule flottante se trouve dans le cœur du processeur, le temps d'exécution peut être négligeable.

Si les logiciels de calcul en virgule flottante sont émulés, l'arithmétique intégrale sera plus rapide.

En cas de doute, profilez.

Faites en sorte que la programmation fonctionne correctement et correctement avant d’optimiser.

1
Thomas Matthews

Il existe certainement une différence entre l'arithmétique à virgule flottante et l'arithmétique entière. Selon le matériel et les micro-instructions spécifiques du processeur, vous obtenez des performances et/ou une précision différentes. Bon termes Google 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 la plate-forme/architecture Word (ou le double), ce qui revient à un int32_t sur x86 et un int64_t sur x86_64. Les processeurs SOme peuvent avoir des instructions intrinsèques qui gèrent plusieurs de ces valeurs à la fois (comme SSE (virgule flottante) et MMX), ce qui accélère les additions ou les multiplications en parallèle.

0
rubenvb

Généralement, les mathématiques entières sont plus rapides que les mathématiques à virgule flottante. En effet, le calcul des nombres entiers implique des calculs plus simples. Cependant, dans la plupart des opérations, on parle de moins d’une douzaine d’horloges. Non millis, micros, nanos ou ticks; horloges. Ceux qui se produisent entre 2-3 milliards de fois par seconde dans les noyaux modernes. En outre, depuis le 486, de nombreux cœurs ont un ensemble d’unités de traitement à virgule flottante ou FPU, qui sont câblés de manière à permettre une arithmétique en virgule flottante efficace, et souvent en parallèle avec la CPU. 

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

0
KeithS

Non, pas vraiment. Cela dépend bien sûr du processeur et du compilateur, mais la différence de performances est généralement négligeable - s'il en existe.

0
Puppy