150 votes

Pourquoi les types ont-ils toujours une certaine taille, quelle que soit leur valeur ?

Les implémentations peuvent différer quant à la taille réelle des types, mais dans la plupart des cas, les types tels que unsigned int et float sont toujours de 4 octets. Mais pourquoi un type occupe-t-il toujours un certains de la mémoire, quelle que soit sa valeur ? Par exemple, si j'ai créé l'entier suivant avec la valeur de 255

int myInt = 255;

Puis myInt occuperait 4 octets avec mon compilateur. Cependant, la valeur réelle, 255 peut être représenté avec seulement 1 octet, alors pourquoi myInt n'occupe pas seulement un octet de mémoire ? Ou la façon plus généralisée de demander : Pourquoi un type n'a qu'une seule taille qui lui est associée alors que l'espace nécessaire pour représenter la valeur pourrait être inférieur à cette taille ?

15 votes

1) " Cependant, la valeur réelle, 256, peut être représentée par un seul octet. " Faux, le plus grand unsinged qui peut être représentée par un octet est la suivante 255 . 2) Considérez les frais généraux liés au calcul de la taille de stockage optimale, et à la réduction/expansion de la zone de stockage, d'une variable, lorsque la valeur change.

99 votes

Eh bien, quand le moment sera venu de lire la valeur de la mémoire, comment pensez-vous que la machine va déterminer le nombre d'octets à lire ? Comment la machine saura-t-elle où arrêter la lecture de la valeur ? Cela nécessitera des installations supplémentaires. Et dans le cas général, la mémoire et les surcharges de performance pour ces installations supplémentaires seront beaucoup plus élevées que dans le cas de l'utilisation de 4 octets fixes pour la lecture de la valeur. unsigned int valeur.

5 votes

Pourquoi un type n'est-il associé qu'à une seule taille alors que l'espace nécessaire pour représenter la valeur peut être inférieur à cette taille ? Parce qu'il se peut que ce ne soit pas toujours plus petit.

139voto

SergeyA Points 2159

Parce que les types représentent fondamentalement le stockage, et qu'ils sont définis en termes de maximum la valeur qu'ils peuvent détenir, et non la valeur actuelle.

L'analogie la plus simple serait celle d'une maison : une maison a une taille fixe, indépendamment du nombre de personnes qui y vivent, et il existe également un code de construction qui stipule le nombre maximum de personnes pouvant vivre dans une maison d'une certaine taille.

Cependant, même si une personne seule vit dans une maison qui peut en accueillir 10, la taille de la maison ne sera pas affectée par le nombre actuel d'occupants.

31 votes

J'aime l'analogie. Si nous l'étendons un peu, nous pourrions imaginer utiliser un langage de programmation qui n'utilise pas de tailles de mémoire fixes pour les types, et cela reviendrait à démolir des pièces de notre maison chaque fois qu'elles ne sont pas utilisées, et à les reconstruire quand c'est nécessaire (c'est-à-dire des tonnes de frais généraux alors que nous pourrions simplement construire un tas de maisons et les laisser debout pour le moment où nous en avons besoin).

5 votes

"Parce que les types représentent fondamentalement le stockage" cela n'est pas vrai pour tous les langages (comme le typecript, par exemple)

56 votes

@corvus_192 les tags ont un sens. Cette question est étiquetée avec C++, pas 'typescript'.

131voto

Useless Points 18909

Le compilateur est censé produire de l'assembleur (et finalement du code machine) pour une certaine machine, et généralement le C++ essaie d'être sympathique à cette machine.

Être sympathique à la machine sous-jacente signifie à peu près : faciliter l'écriture de code C++ qui s'adaptera efficacement aux opérations que la machine peut exécuter rapidement. Ainsi, nous voulons fournir un accès aux types de données et aux opérations qui sont rapides et "naturelles" sur notre plate-forme matérielle.

Concrètement, considérons une architecture de machine spécifique. Prenons la famille actuelle des Intel x86.

The Intel® 64 and IA-32 Architectures Software Developer's Manual vol 1 ( enlace ), la section 3.4.1 dit :

Les registres 32 bits à usage général EAX, EBX, ECX, EDX, ESI, EDI, EBP et ESP sont prévus pour contenir les éléments suivants éléments suivants :

- Opérandes pour les opérations logiques et arithmétiques

- Opérandes pour les calculs d'adresses

- Pointeurs mémoire

Nous voulons donc que le compilateur utilise ces registres EAX, EBX, etc. lorsqu'il compile l'arithmétique entière simple du C++. Cela signifie que lorsque je déclare un int Il faut que ce soit quelque chose de compatible avec ces registres, afin que je puisse les utiliser efficacement.

Les registres sont toujours de la même taille (ici, 32 bits), donc mes int Les variables seront toujours en 32 bits également. J'utiliserai la même disposition (little-endian) pour ne pas avoir à faire une conversion chaque fois que je charge une valeur de variable dans un registre, ou que je stocke un registre dans une variable.

Utilisation de godbolt nous pouvons voir exactement ce que le compilateur fait pour un code trivial :

int square(int num) {
    return num * num;
}

compile (avec GCC 8.1 et -fomit-frame-pointer -O3 pour simplifier) à :

square(int):
  imul edi, edi
  mov eax, edi
  ret

cela signifie :

  1. le site int num a été passé en registre EDI, ce qui signifie qu'il a exactement la taille et la disposition qu'Intel attend d'un registre natif. La fonction n'a pas besoin de convertir quoi que ce soit
  2. la multiplication est une instruction unique ( imul ), ce qui est très rapide
  3. le retour du résultat consiste simplement à le copier dans un autre registre (l'appelant s'attend à ce que le résultat soit placé dans EAX).

Edit : nous pouvons ajouter une comparaison pertinente pour montrer la différence que fait l'utilisation d'une mise en page non native. Le cas le plus simple est de stocker les valeurs dans une largeur autre que la largeur native.

Utilisation de godbolt Encore une fois, nous pouvons comparer une simple multiplication native

unsigned mult (unsigned x, unsigned y)
{
    return x*y;
}

mult(unsigned int, unsigned int):
  mov eax, edi
  imul eax, esi
  ret

avec le code équivalent pour une largeur non standard

struct pair {
    unsigned x : 31;
    unsigned y : 31;
};

unsigned mult (pair p)
{
    return p.x*p.y;
}

mult(pair):
  mov eax, edi
  shr rdi, 32
  and eax, 2147483647
  and edi, 2147483647
  imul eax, edi
  ret

Toutes les instructions supplémentaires servent à convertir le format d'entrée (deux entiers non signés de 31 bits) dans le format que le processeur peut gérer en natif. Si nous voulions stocker le résultat dans une valeur de 31 bits, il y aurait une ou deux autres instructions pour le faire.

Cette complexité supplémentaire signifie que vous ne vous en soucierez que si le gain de place est très important. Dans ce cas, nous n'économisons que deux bits par rapport à l'utilisation de la fonction native unsigned o uint32_t ce qui aurait généré un code beaucoup plus simple.


Une note sur les tailles dynamiques :

Dans l'exemple ci-dessus, il s'agit toujours de valeurs à largeur fixe plutôt que de valeurs à largeur variable, mais la largeur (et l'alignement) ne correspondent plus aux registres natifs.

La plate-forme x86 possède plusieurs tailles natives, notamment 8 bits et 16 bits en plus de la taille principale 32 bits (je passe sous silence le mode 64 bits et diverses autres choses pour simplifier).

Ces types (char, int8_t, uint8_t, int16_t etc.) sont également directement pris en charge par l'architecture - en partie pour assurer la compatibilité avec les anciens jeux d'instructions 8086/286/386/etc. etc. etc.

C'est certainement le cas de choisir le plus petit naturel à taille fixe Il s'agit toujours de chargements et de stockages rapides à instructions uniques, vous bénéficiez toujours de l'arithmétique native à pleine vitesse et vous pouvez même améliorer les performances en réduisant les manques de cache.

C'est très différent du codage à longueur variable - j'ai travaillé avec certains d'entre eux, et ils sont horribles. Chaque chargement devient une boucle au lieu d'une seule instruction. Chaque stockage est également une boucle. Chaque structure est à longueur variable, donc vous ne pouvez pas utiliser les tableaux naturellement.


Une note supplémentaire sur l'efficacité

Dans les commentaires suivants, vous avez utilisé le mot "efficace", pour autant que je sache, en ce qui concerne la taille du stockage. Nous choisissons parfois de minimiser la taille du stockage - cela peut être important lorsque nous sauvegardons un très grand nombre de valeurs dans des fichiers, ou lorsque nous les envoyons sur un réseau. La contrepartie est que nous devons charger ces valeurs dans des registres pour hacer rien avec eux, et effectuer la conversion n'est pas gratuit.

Lorsque nous discutons d'efficacité, nous devons savoir ce que nous optimisons et quels sont les compromis à faire. L'utilisation de types de stockage non natifs est une façon de troquer la vitesse de traitement contre l'espace, et elle est parfois judicieuse. L'utilisation d'un stockage de longueur variable (pour les types arithmétiques au moins) permet de troquer la vitesse de traitement contre l'espace. plus la vitesse de traitement (ainsi que la complexité du code et le temps des développeurs) pour un gain d'espace supplémentaire souvent minime.

La pénalité de vitesse que vous payez pour cela signifie que cela ne vaut la peine que lorsque vous devez absolument minimiser la bande passante ou le stockage à long terme, et dans ces cas-là, il est généralement plus facile d'utiliser un format simple et naturel, puis de le compresser avec un système général (comme zip, gzip, bzip2, xy ou autre).


en résumé

Chaque plate-forme possède une architecture, mais vous pouvez imaginer un nombre essentiellement illimité de manières différentes de représenter les données. Il n'est pas raisonnable qu'un langage fournisse un nombre illimité de types de données intégrés. Ainsi, le C++ fournit un accès implicite à l'ensemble naturel et natif des types de données de la plate-forme, et vous permet de coder vous-même toute autre représentation (non native).

0 votes

Je regarde toutes les belles réponses en essayant de leur donner un sens En ce qui concerne votre réponse, une taille dynamique, disons inférieure à 32 bits pour un entier, ne permettrait-elle pas simplement d'avoir plus de variables dans un registre ? Si l'endienneté est la même, pourquoi cela ne serait-il pas optimal ?

0 votes

@asd C++ est sympathique aux opérations (instructions) fournies par la machine, pas seulement à son stockage. Il n'est pas facile d'effectuer des opérations sur plusieurs valeurs dans un seul registre général, et les instructions SIMD (vectorielles) opèrent sur des registres contenant de nombreuses copies d'un seul type (taille). Ainsi, toute flexibilité dans la taille dynamique d'un int ne pourrait pas être exploitée, de toute façon.

7 votes

@asd mais combien de registres utiliserez-vous dans le code qui détermine combien de variables sont actuellement stockées dans un registre ?

44voto

Loki Astari Points 116129

Il s'agit d'une optimisation et d'une simplification.

Vous pouvez soit avoir des objets de taille fixe. Ainsi, la valeur est stockée.
Ou vous pouvez avoir des objets de taille variable. Mais en stockant la valeur et la taille.

objets de taille fixe

Le code qui manipule les nombres ne doit pas se soucier de la taille. On part du principe qu'on utilise toujours 4 octets et on rend le code très simple.

Objets de taille dynamique

Le code qui manipule les nombres doit comprendre que lorsqu'il lit une variable, il doit lire la valeur et la taille. Utilisez la taille pour vous assurer que tous les bits de poids fort sont à zéro dans le registre.

Lorsque vous replacez la valeur en mémoire, si la valeur n'a pas dépassé sa taille actuelle, il suffit de la replacer en mémoire. Mais si la valeur a diminué ou augmenté, vous devez déplacer l'emplacement de stockage de l'objet vers un autre emplacement en mémoire pour vous assurer qu'il ne déborde pas. Vous devez maintenant suivre la position de ce nombre (car il peut se déplacer s'il devient trop grand pour sa taille). Vous devez également suivre tous les emplacements de variables inutilisés afin qu'ils puissent être réutilisés.

Résumé

Le code généré pour les objets de taille fixe est beaucoup plus simple.

Note

La compression utilise le fait que 255 tiendront dans un octet. Il existe des schémas de compression pour le stockage de grands ensembles de données qui utilisent activement différentes valeurs de taille pour différents nombres. Mais comme il ne s'agit pas de données réelles, vous n'avez pas les complexités décrites ci-dessus. Vous utilisez moins d'espace pour stocker les données, au prix de la compression/décompression des données pour le stockage.

4 votes

C'est la meilleure réponse pour moi : Comment gardez-vous la trace de la taille ? Avec plus la mémoire ?

0 votes

@ThomasMoors Oui, exactement : avec plus la mémoire. Si vous disposez, par exemple, d'un tableau dynamique, alors une partie de la mémoire de l'entreprise peut être utilisée. int stockera le nombre d'éléments dans ce tableau. Ce int aura à nouveau une taille fixe.

1 votes

Il y a deux options couramment utilisées, qui nécessitent toutes deux de la mémoire supplémentaire : soit vous avez un champ (de taille fixe) qui vous indique la quantité de données (par exemple, un int pour la taille du tableau, ou des chaînes de type "pascal" où le premier élément contient le nombre de caractères), soit vous pouvez avoir une chaîne (ou une structure plus complexe) dans laquelle chaque note d'une manière ou d'une autre s'il s'agit du dernier élément - par exemple, les chaînes de caractères à terminaison zéro, ou la plupart des formes de listes liées.

28voto

mtraceur Points 424

En effet, dans un langage comme le C++, l'objectif de conception est que les opérations simples soient compilées en instructions machine simples.

Tous les jeux d'instructions des CPU classiques fonctionnent avec largeur fixe et si vous voulez faire largeur variable vous devez faire plusieurs instructions machine pour les gérer.

Quant à pourquoi le matériel informatique sous-jacent est comme ça : C'est parce que c'est plus simple, et plus efficace pour beaucoup de cas (mais pas tous).

Imaginez l'ordinateur comme un morceau de ruban adhésif :

| xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | ...

Si vous dites simplement à l'ordinateur de regarder le premier octet de la bande, xx comment sait-il si le type s'arrête là ou s'il passe à l'octet suivant ? Si vous avez un nombre comme 255 (hexadécimal FF ) ou un nombre comme 65535 (hexadécimal FFFF ), le premier octet est toujours FF .

Alors comment le savez-vous ? Soit vous choisissez une taille et vous vous y tenez, soit vous devez ajouter une logique supplémentaire et "surcharger" la signification d'au moins un bit ou un octet pour indiquer que la valeur continue sur l'octet suivant. Cette logique n'est jamais "gratuite", soit vous l'émulez en logiciel, soit vous ajoutez un tas de transistors supplémentaires au CPU pour le faire.

Les types à largeur fixe des langages tels que C et C++ reflètent cette situation.

Ce n'est pas le cas. ont Les langages plus abstraits, qui se soucient moins de l'efficacité maximale du code, sont libres d'utiliser des codages à largeur variable (également connus sous le nom de "Variable Length Quantities" ou VLQ) pour les types numériques.

Pour en savoir plus : Si vous cherchez "quantité de longueur variable", vous trouverez quelques exemples où ce type d'encodage est utilisé. est réellement efficace et qui vaut la logique supplémentaire. C'est généralement le cas lorsque vous devez stocker un grand nombre de valeurs qui peuvent se situer n'importe où dans une large gamme, mais la plupart des valeurs tendent vers une petite sous-gamme.


Notez que si un compilateur peut prouver qu'il peut s'en sortir en stockant la valeur dans un espace plus petit sans casser le code (par exemple, il s'agit d'une variable uniquement visible en interne dans une seule unité de traduction), y son heuristique d'optimisation suggère qu'il sera plus efficace sur le matériel cible, il est entièrement autorisé à l'optimiser en conséquence et le stocker dans un espace plus réduit, tant que le reste du code fonctionne "comme si" il faisait la chose standard.

Mais quand le code doit inter-opérer avec d'autres codes qui pourraient être compilés séparément, les tailles doivent rester cohérentes, ou s'assurer que chaque morceau de code suit la même convention.

Parce que si ce n'est pas cohérent, il y a cette complication : Que faire si j'ai int x = 255; mais plus tard dans le code, je fais x = y ? Si int pourrait être de largeur variable, le compilateur devrait savoir à l'avance de pré-allouer la quantité maximale d'espace dont il aura besoin. Ce n'est pas toujours possible, car que faire si y est un argument passé depuis un autre morceau de code qui est compilé séparément ?

26voto

Bill K Points 32115

Java utilise des classes appelées "BigInteger" et "BigDecimal" pour faire exactement cela, tout comme l'interface de classe C++ GMP C++ apparemment (merci Digital Trauma). Vous pouvez facilement le faire vous-même dans presque tous les langages si vous le souhaitez.

Les CPU ont toujours eu la capacité d'utiliser BCD (Binary Coded Decimal) qui est conçu pour supporter des opérations de n'importe quelle longueur (mais vous avez tendance à opérer manuellement sur un octet à la fois, ce qui serait LENT selon les normes actuelles des GPU).

La raison pour laquelle nous n'utilisons pas ces solutions ou d'autres solutions similaires ? Les performances. Vos langages les plus performants ne peuvent pas se permettre d'étendre une variable au milieu d'une opération de boucle serrée - ce serait très non-déterministe.

Dans les situations de stockage et de transport de masse, les valeurs emballées sont souvent le SEUL type de valeur à utiliser. Par exemple, un paquet de musique/vidéo diffusé en continu sur votre ordinateur peut utiliser un bit pour spécifier si la valeur suivante est de 2 ou de 4 octets afin d'optimiser la taille.

Une fois qu'il est sur votre ordinateur où il peut être utilisé, la mémoire est bon marché mais la vitesse et la complication des variables redimensionnables ne le sont pas c'est vraiment la seule raison.

4 votes

Heureux de voir que quelqu'un mentionne BigInteger. Ce n'est pas que c'est une idée stupide, c'est juste que cela n'a de sens que pour des nombres extrêmement grands.

1 votes

Pour être pédant, vous voulez en fait dire des nombres extrêmement précis :) Enfin, au moins dans le cas de BigDecimal...

2 votes

Et puisque c'est marqué c++ il est probablement utile de mentionner le Interface de classe GMP C++ qui est la même idée que le Big* de Java.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X