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 :
- 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
- la multiplication est une instruction unique (
imul
), ce qui est très rapide
- 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).
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 suivante255
. 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.
74 votes
J'aime beaucoup cette question. Même s'il peut sembler simple d'y répondre, je pense que donner une explication précise nécessite une bonne compréhension du fonctionnement des ordinateurs et des architectures informatiques. La plupart des gens vont probablement considérer que cela va de soi, sans avoir une explication complète.
2 votes
FYI - Sur Ubuntu 17.10, sizeof (std::string) rapporte 32 octets utilisés en mémoire automatique, quel que soit le nombre de caractères qu'il contient. (Tous les caractères de données sont en mémoire dynamique !) Mais c'est un détail d'implémentation. Des détails similaires existent pour std::vector et de nombreux autres conteneurs.
0 votes
@AlgirdasPreidžius 1) Ahh oui, je voulais dire qu'un octet peut représenter 256 valeurs différentes. Je vais modifier la question pour être plus précis. 2) Je vois, mais vous pourriez également économiser de la mémoire, donc les avantages et inconvénients des tailles dynamiques pourraient être équivalents aux avantages et inconvénients des tailles statiques. Ainsi, le type de stockage dépendrait de la situation dans laquelle l'un est favorisé par rapport à l'autre.
3 votes
@asd 1) Le stockage n'est qu'un côté de l'équation. La vitesse de calcul en est un autre. Dans un cas typique, la vitesse de calcul est plus importante que l'espace de stockage. Alors, pourquoi payer pour ce dont on n'a pas besoin ? 2) Les types de
char
,short
etc. existent pour une raison : si vous savez que les nombres sur lesquels vous opérez sont suffisamment petits, vous pouvez utiliser un type de données plus petit. 3) Pensez à lire les autres commentaires/réponses. Dans un cas typique : Cela ne vaut tout simplement pas la peine de s'embêter.37 votes
Considérez ce qui se passerait si vous ajoutiez 1 à la valeur de la variable, ce qui la porterait à 256, et il faudrait donc l'étendre. Où doit-elle s'étendre ? Déplacez-vous le reste de la mémoire pour faire de la place ? La variable elle-même se déplace-t-elle ? Si c'est le cas, où se déplace-t-elle et comment trouver les pointeurs que vous devez mettre à jour ?
3 votes
Les types en général n'ont pas de taille constante. int, float, etc. en ont. Beaucoup d'autres ont une taille constante en c++, contrairement à d'autres langages, pour des raisons de performance. D'autres types ont une taille variable, même en c++, parce qu'ils en ont besoin, par exemple std::vector.
13 votes
@someidiot non, vous avez tort.
std::vector<X>
a toujours la même taille, c'est-à-dire quesizeof(std::vector<X>)
est une constante de temps de compilation.5 votes
L'explication me manque : il est stocké comme 4 octets parce que "int" a donné l'ordre explicite de le faire.
3 votes
Varints du tampon de protocole sont un exemple de mise en œuvre d'un quantité variable où " Les petits nombres prennent un plus petit nombre d'octets. " comme vous le décrivez.
2 votes
Si vous achetez une calculatrice à huit chiffres, devient-elle une calculatrice à trois chiffres si vous entrez la valeur 255 ? J'en doute.
2 votes
@SergeyA Je ne suis pas d'accord. De toute évidence,
sizeof(std::vector<X>)
est une constante de temps de compilation, mais c'est parce quesizeof
ne vous indique pas avec précision la quantité de mémoire occupée par le type. Ce c'est plutôtsizeof(vec) + vec.capacity()*(sizeof(vec.front())) + vec.capacity() ? dynamic_memory_overhead : 0
4 votes
@MartinBonner vous êtes libre de ne pas être d'accord comme vous voulez, mais en termes C++, la taille du type est la valeur renvoyée par
sizeof
opérateur. Il s'agit d'une définition de la norme.0 votes
255 et vous voulez utiliser 2 grignons pour cela. OK, je vois ça. Combien voulez-vous en utiliser pour le 9 ? Combien pour le zéro ?
1 votes
Les types de données et leur mise en correspondance avec la mémoire sont très liés à la programmation ; il ne s'agit pas d'un sujet relatif au "matériel et aux logiciels informatiques généraux". Ce motif proche est destiné aux personnes qui demandent comment faire fonctionner leur tableur, par exemple. Vote pour la réouverture.
0 votes
Je suis d'accord avec @WayneConrad - je ne vois pas comment la raison de proximité s'applique ici. Il me semble que c'est une question parfaitement raisonnable.
0 votes
Juste pour étouffer toute dispute potentielle... SergeyA et Martin Bonner ont tous deux raison.
std:vector<T>
encapsule un tableau alloué de manière dynamique, tel que généré parnew T[N]
typiquement en stockant une poignée vers ledit tableau.std::vector
La taille de l'entreprise est donc constante, et mesurée avec précision parsizeof
. Cependant, comme le stockage réel des données géré parvector
n'est pas dans levector
lui-même, il ne sera pas reflété par le résultat desizeof
.0 votes
Même si vous pouviez stocker les données sur 8 bits, sur la plupart des systèmes, vous n'avez pas la possibilité de lire les données sur 8 bits à la fois, car les processeurs ont généralement un bus de données de largeur fixe (comme 32 bits). Vous finirez par lire 32 bits à partir du stockage et par "ignorer" 24 bits avec votre schéma, ce qui rendra toute cette "optimisation" inutile.