Les implémentations peuvent différer entre les tailles réelles des types, mais sur la plupart, les types tels que unsigned int et float sont toujours de 4 octets. Mais pourquoi un type occupe-t-il toujours une certaine quantité de mémoire, quelle que soit sa valeur? Par exemple, si j'ai créé l'entier suivant avec la valeur 255
int myInt = 255;
Alors myInt
occuperait 4 octets avec mon compilateur. Cependant, la valeur réelle 255
peut être représentée avec seulement 1 octet, alors pourquoi ne myInt
pas occuper simplement 1 octet de mémoire? Ou la manière plus générale de demander: pourquoi un type n'a-t-il qu'une seule taille associée alors que l'espace requis pour représenter la valeur peut être inférieur à cette taille?
unsinged
valeur qui peut être représentée avec 1 octet est255
. 2) Tenez compte de la surcharge liée au calcul de la taille de stockage optimale et à la réduction / extension de la zone de stockage d'une variable à mesure que la valeur change.unsigned int
valeur.std::vector<X>
a toujours la même taille, c'estsizeof(std::vector<X>)
-à- dire est une constante de compilation.Réponses:
Le compilateur est censé produire l'assembleur (et finalement le code machine) pour certaines machines, et généralement C ++ essaie d'être sympathique avec cette machine.
Être sympathique à la machine sous-jacente signifie en gros: faciliter l'écriture de code C ++ qui mappera efficacement sur les opérations que la machine peut exécuter rapidement. Nous souhaitons donc fournir un accès aux types de données et aux opérations rapides et «naturelles» sur notre plate-forme matérielle.
Concrètement, considérons une architecture de machine spécifique. Prenons la famille Intel x86 actuelle.
Le manuel du développeur de logiciels des architectures Intel® 64 et IA-32 vol 1 ( lien ), section 3.4.1 dit:
Donc, nous voulons que le compilateur utilise ces registres EAX, EBX etc. lorsqu'il compile une arithmétique d'entiers C ++ simple. Cela signifie que lorsque je déclare un
int
, ce doit être quelque chose de compatible avec ces registres, afin que je puisse les utiliser efficacement.Les registres ont toujours la même taille (ici, 32 bits), donc mes
int
variables seront toujours de 32 bits également. J'utiliserai la même disposition (petit-boutiste) 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.En utilisant godbolt, nous pouvons voir exactement ce que fait le compilateur pour un code trivial:
compile (avec GCC 8.1 et
-fomit-frame-pointer -O3
par souci de simplicité) pour:ça signifie:
int num
paramètre a été passé dans le registre EDI, ce qui signifie que c'est exactement la taille et la disposition qu'Intel attend d'un registre natif. La fonction n'a rien à convertirimul
), qui est très rapideEdit: nous pouvons ajouter une comparaison pertinente pour montrer la différence en utilisant une mise en page non native. Le cas le plus simple est de stocker des valeurs dans autre chose que la largeur native.
En utilisant à nouveau godbolt , nous pouvons comparer une simple multiplication native
avec le code équivalent pour une largeur non standard
Toutes les instructions supplémentaires concernent la conversion du format d'entrée (deux entiers non signés de 31 bits) dans le format que le processeur peut gérer nativement. Si nous voulions stocker le résultat dans une valeur de 31 bits, il y aurait une ou deux instructions supplémentaires pour le faire.
Cette complexité supplémentaire signifie que vous ne vous soucierez de cela que lorsque l'économie d'espace est très importante. Dans ce cas, nous n'économisons que deux bits par rapport à l'utilisation du natif
unsigned
ou duuint32_t
type, ce qui aurait généré un code beaucoup plus simple.Une note sur les tailles dynamiques:
L'exemple ci-dessus est toujours des valeurs de largeur fixe plutôt que de largeur variable, mais la largeur (et l'alignement) ne correspondent plus aux registres natifs.
La plate-forme x86 a plusieurs tailles natives, y compris 8 bits et 16 bits en plus du 32 bits principal (je passe sous silence le mode 64 bits et diverses autres choses pour plus de simplicité).
Ces types (char, int8_t, uint8_t, int16_t etc.) sont également directement pris en charge par l'architecture - en partie pour la compatibilité avec les anciens 8086/286/386 / etc. jeux d'instructions etc.
C'est certainement le cas que le choix du plus petit type de taille fixe naturel qui suffira, peut être une bonne pratique - ils sont toujours rapides, des instructions uniques se chargent et se stockent, vous obtenez toujours une arithmétique native à pleine vitesse, et vous pouvez même améliorer les performances en réduire les échecs 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 charge devient une boucle au lieu d'une seule instruction. Chaque magasin est aussi une boucle. Chaque structure est de longueur variable, vous ne pouvez donc pas utiliser de tableaux naturellement.
Une autre note sur l'efficacité
Dans les commentaires suivants, vous avez utilisé le mot «efficace», pour autant que je sache en ce qui concerne la taille de stockage. Nous choisissons parfois de minimiser la taille de stockage - cela peut être important lorsque nous enregistrons un très grand nombre de valeurs dans des fichiers ou que nous les envoyons sur un réseau. Le compromis est que nous devons charger ces valeurs dans des registres pour faire quoi que ce soit avec elles, et effectuer la conversion n'est pas gratuite.
Lorsque nous discutons d'efficacité, nous devons savoir ce que nous optimisons et quels sont les compromis. L'utilisation de types de stockage non natifs est un moyen d'échanger la vitesse de traitement contre de l'espace, et cela a parfois du sens. En utilisant un stockage de longueur variable (pour les types arithmétiques au moins), échange plus de vitesse de traitement (et de complexité du code et de temps de développeur) pour une économie 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, il est généralement plus facile d'utiliser un format simple et naturel - puis de le compresser simplement avec un système à usage général. (comme zip, gzip, bzip2, xy ou autre).
tl; dr
Chaque plate-forme a une architecture, mais vous pouvez proposer un nombre essentiellement illimité de façons différentes de représenter les données. Il n'est raisonnable pour aucune langue de fournir un nombre illimité de types de données intégrés. Ainsi, C ++ fournit un accès implicite à l'ensemble de types de données natif et naturel de la plate-forme et vous permet de coder vous-même toute autre représentation (non native).
la source
git
les métadonnées de), soit vous les gardez en mémoire et vous avez parfois besoin d'accéder au hasard ou de modifier quelques-unes mais pas la plupart les valeurs (comme dans les moteurs de rendu HTML + CSS), et ne peuvent donc être évités qu'en utilisant quelque chose comme VLQ in-place.Parce que les types représentent fondamentalement le stockage et qu'ils sont définis en termes de valeur maximale qu'ils peuvent contenir, et non de valeur actuelle.
L'analogie très simple serait une maison - une maison a une taille fixe, quel que soit le nombre de personnes qui y vivent, et il existe également un code du bâtiment 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 pouvant en accueillir 10, la taille de la maison ne sera pas affectée par le nombre actuel d'occupants.
la source
C'est une optimisation et une simplification.
Vous pouvez avoir des objets de taille fixe. Stockant ainsi la valeur.
Ou vous pouvez avoir des objets de taille variable. Mais stocker la valeur et la taille.
objets de taille fixe
Le code qui manipule le nombre n'a pas besoin de se soucier de la taille. Vous supposez que vous utilisez toujours 4 octets et que vous rendez le code très simple.
Objets de taille dynamique
Le code que le nombre manipule doit comprendre lors de la lecture d'une variable qu'il doit lire la valeur et la taille. Utilisez la taille pour vous assurer que tous les bits hauts sont à zéro dans le registre.
Lorsque vous remettez la valeur en mémoire si la valeur n'a pas dépassé sa taille actuelle, remettez simplement la valeur 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 de la 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 potentiellement être réutilisés.
Résumé
Le code généré pour les objets de taille fixe est beaucoup plus simple.
Remarque
La compression utilise le fait que 255 tient dans un octet. Il existe des schémas de compression pour stocker de grands ensembles de données qui utiliseront activement différentes valeurs de taille pour différents nombres. Mais comme il ne s'agit pas de données en direct, 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.
la source
int
stockeront le nombre d'éléments dans ce tableau. Celaint
lui - même aura à nouveau une taille fixe.Parce que dans un langage comme C ++, un objectif de conception est que les opérations simples se compilent en instructions machine simples.
Tous les jeux d'instructions CPU grand public fonctionnent avec des types à largeur fixe , et si vous voulez faire des types à largeur variable , vous devez faire plusieurs instructions machine pour les gérer.
Quant à savoir pourquoi le matériel informatique sous-jacent est de cette façon: c'est parce qu'il est plus simple et plus efficace dans de nombreux cas (mais pas tous).
Imaginez l'ordinateur comme un morceau de ruban adhésif:
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 comme255
(hexadécimalFF
) ou un nombre comme65535
(hexadécimalFFFF
), le premier octet est toujoursFF
.Alors comment le savez-vous? Vous devez ajouter une logique supplémentaire et «surcharger» la signification d'au moins un bit ou une valeur d'octet pour indiquer que la valeur continue jusqu'à l'octet suivant. Cette logique n'est jamais «gratuite», soit vous l'émulez dans le logiciel, soit vous ajoutez un tas de transistors supplémentaires au CPU pour le faire.
Les types de langages à largeur fixe comme C et C ++ reflètent cela.
Il n'est pas nécessaire que ce soit ainsi, et les langages plus abstraits qui sont moins concernés par le mappage vers un code à efficacité maximale sont libres d'utiliser des codages à largeur variable (également connus sous le nom de «Quantités de longueur variable» ou VLQ) pour les types numériques.
Lectures complémentaires: Si vous recherchez "quantité de longueur variable", vous pouvez trouver des exemples où ce type de codage est réellement efficace et vaut la logique supplémentaire. C'est généralement lorsque vous devez stocker une énorme quantité de valeurs qui peuvent être n'importe où dans une large plage, mais la plupart des valeurs tendent vers une petite sous-plage.
Notez que si un compilateur peut prouver qu'il peut s'en tirer en stockant la valeur dans une plus petite quantité d'espace sans casser le code (par exemple, c'est une variable visible uniquement en interne dans une seule unité de traduction), et ses heuristiques d'optimisation suggèrent qu'il ' ll sera plus efficace sur le matériel cible, il est tout à fait autorisé de l' optimiser en conséquence et de le stocker dans un espace plus petit, tant que le reste du code fonctionne "comme si" il faisait la chose standard.
Mais , lorsque le code doit interagir avec un autre code qui pourrait être compilé séparément, les tailles doivent rester cohérentes ou garantir 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 je l'ai
int x = 255;
mais plus tard dans le code que je faisx = y
? Si celaint
pouvait être de largeur variable, le compilateur devrait savoir à l'avance pour pré-allouer la quantité maximale d'espace dont il aura besoin. Ce n'est pas toujours possible, car quey
se passe- t-il si un argument est transmis à partir d'un autre morceau de code compilé séparément?la source
Java utilise des classes appelées "BigInteger" et "BigDecimal" pour faire exactement cela, comme le fait apparemment l'interface de classe GMP C ++ de C ++ (merci Digital Trauma). Vous pouvez facilement le faire vous-même dans à peu près n'importe quelle langue si vous le souhaitez.
Les processeurs ont toujours eu la possibilité d'utiliser BCD (Binary Coded Decimal) qui est conçu pour prendre en charge 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 GPU actuelles.)
La raison pour laquelle nous n'utilisons pas ces solutions ou d'autres solutions similaires? Performance. Vos langages les plus performants ne peuvent pas se permettre d'étendre une variable au milieu d'une opération en boucle serrée - ce serait très non déterministe.
Dans les situations de stockage de masse et de transport, les valeurs emballées sont souvent le SEUL type de valeur que vous utiliseriez. Par exemple, un paquet de musique / vidéo diffusé sur votre ordinateur peut dépenser un peu pour spécifier si la valeur suivante est de 2 ou 4 octets en tant qu'optimisation de la taille.
Une fois qu'elle est sur votre ordinateur où elle peut être utilisée, 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.
la source
Parce que ce serait très compliqué et lourd de calculs d'avoir des types simples avec des tailles dynamiques. Je ne suis pas sûr que ce soit même possible.
L'ordinateur devrait vérifier combien de bits le nombre prend après chaque changement de sa valeur. Ce serait beaucoup d'opérations supplémentaires. Et il serait beaucoup plus difficile d'effectuer des calculs lorsque vous ne connaissez pas la taille des variables lors de la compilation.
Pour prendre en charge les tailles dynamiques des variables, l'ordinateur devrait en fait se souvenir du nombre d'octets d'une variable en ce moment, ce qui nécessiterait de la mémoire supplémentaire pour stocker ces informations. Et cette information devrait être analysée avant chaque opération sur la variable pour choisir la bonne instruction du processeur.
Pour mieux comprendre le fonctionnement de l'ordinateur et pourquoi les variables ont des tailles constantes, apprenez les bases du langage assembleur.
Bien que, je suppose qu'il serait possible de réaliser quelque chose comme ça avec des valeurs constexpr. Cependant, cela rendrait le code moins prévisible pour un programmeur. Je suppose que certaines optimisations du compilateur peuvent faire quelque chose comme ça, mais elles le cachent à un programmeur pour garder les choses simples.
Je n'ai décrit ici que les problèmes qui concernent la performance d'un programme. J'ai omis tous les problèmes qui devraient être résolus pour économiser de la mémoire en réduisant la taille des variables. Honnêtement, je ne pense pas que ce soit même possible.
En conclusion, l'utilisation de variables plus petites que celles déclarées n'a de sens que si leurs valeurs sont connues lors de la compilation. Il est fort probable que les compilateurs modernes le fassent. Dans d'autres cas, cela entraînerait trop de problèmes difficiles, voire insolubles.
la source
56
et nous la multiplions par une variable de 2 octets. Sur certaines architectures, le fonctionnement 64 bits serait plus lourd en calcul, donc le compilateur pourrait l'optimiser pour n'effectuer qu'une multiplication 16 bits.Ceci est connu sous le nom de codage à longueur variable , il existe différents codages définis, par exemple VLQ . L'un des plus célèbres, cependant, est probablement UTF-8 : UTF-8 encode des points de code sur un nombre variable d'octets, de 1 à 4.
Comme toujours en ingénierie, tout est question de compromis. Il n'y a pas de solution qui n'a que des avantages, vous devez donc trouver un équilibre entre avantages et compromis lors de la conception de votre solution.
La conception qui a été choisie consistait à utiliser des types fondamentaux de taille fixe, et le matériel / les langages se sont envolés à partir de là.
Alors, quelle est la faiblesse fondamentale de l'encodage variable , qui a conduit à son rejet au profit de schémas plus gourmands en mémoire? Pas d'adressage aléatoire .
Quel est l'index de l'octet auquel le 4e point de code commence dans une chaîne UTF-8?
Cela dépend des valeurs des points de code précédents, un balayage linéaire est nécessaire.
Il existe sûrement des schémas de codage à longueur variable qui sont meilleurs pour l'adressage aléatoire?
Oui, mais ils sont aussi plus compliqués. S'il y en a un idéal, je ne l'ai encore jamais vu.
L'adressage aléatoire est-il vraiment important de toute façon?
Oh oui!
Le fait est que tout type d'agrégat / tableau repose sur des types de taille fixe:
struct
? Adressage aléatoire!Ce qui signifie que vous avez essentiellement le compromis suivant:
Types de taille fixe OU analyses de mémoire linéaire
la source
La mémoire de l'ordinateur est subdivisée en blocs adressés consécutivement d'une certaine taille (souvent 8 bits et appelés octets), et la plupart des ordinateurs sont conçus pour accéder efficacement aux séquences d'octets qui ont des adresses consécutives.
Si l'adresse d'un objet ne change jamais pendant la durée de vie de l'objet, alors le code donné à son adresse peut accéder rapidement à l'objet en question. Une limitation essentielle de cette approche, cependant, est que si une adresse est attribuée pour l'adresse X, puis une autre adresse est attribuée pour l'adresse Y qui est éloignée de N octets, alors X ne pourra pas dépasser N octets pendant la durée de vie. de Y, sauf si X ou Y est déplacé. Pour que X se déplace, il serait nécessaire que tout dans l'univers contenant l'adresse de X soit mis à jour pour refléter la nouvelle, et de même pour que Y se déplace. Bien qu'il soit possible de concevoir un système pour faciliter de telles mises à jour (Java et .NET le gèrent assez bien), il est beaucoup plus efficace de travailler avec des objets qui resteront au même emplacement tout au long de leur vie,
la source
La réponse courte est: parce que la norme C ++ le dit.
La réponse longue est: ce que vous pouvez faire sur un ordinateur est finalement limité par le matériel. Il est, bien sûr, possible d'encoder un entier dans un nombre variable d'octets pour le stockage, mais sa lecture nécessiterait soit des instructions CPU spéciales pour être performante, soit vous pourriez l'implémenter dans un logiciel, mais alors ce serait terriblement lent. Des opérations de taille fixe sont disponibles dans la CPU pour charger des valeurs de largeurs prédéfinies, il n'y en a aucune pour des largeurs variables.
Un autre point à considérer est le fonctionnement de la mémoire de l'ordinateur. Disons que votre type entier peut prendre entre 1 et 4 octets de stockage. Supposons que vous stockiez la valeur 42 dans votre entier: cela prend 1 octet, et vous le placez à l'adresse mémoire X. Ensuite, vous stockez votre prochaine variable à l'emplacement X + 1 (je ne considère pas l'alignement à ce stade) et ainsi de suite . Plus tard, vous décidez de changer votre valeur en 6424.
Mais cela ne tient pas dans un seul octet! Donc que fais-tu? Où mettez-vous le reste? Vous avez déjà quelque chose à X + 1, vous ne pouvez donc pas le placer ici. Ailleurs? Comment saurez-vous plus tard où? La mémoire de l'ordinateur ne prend pas en charge la sémantique d'insertion: vous ne pouvez pas simplement placer quelque chose à un emplacement et tout mettre de côté pour faire de la place!
A part: Ce dont vous parlez est en fait le domaine de la compression des données. Des algorithmes de compression existent pour tout regrouper plus étroitement, donc au moins certains d'entre eux envisageront de ne pas utiliser plus d'espace pour votre entier qu'il n'en a besoin. Cependant, les données compressées ne sont pas faciles à modifier (si possible du tout) et finissent simplement par être recompressées chaque fois que vous y apportez des modifications.
la source
Cela présente des avantages substantiels en termes de performances d'exécution. Si vous deviez opérer sur des types de taille variable, vous devrez décoder chaque nombre avant de faire l'opération (les instructions de code machine sont généralement de largeur fixe), faire l'opération, puis trouver un espace mémoire suffisamment grand pour contenir le résultat. Ce sont des opérations très difficiles. Il est beaucoup plus facile de simplement stocker toutes les données de manière légèrement inefficace.
Ce n'est pas toujours ainsi que cela se fait. Considérez le protocole Protobuf de Google. Les protobufs sont conçus pour transmettre des données de manière très efficace. Diminuer le nombre d'octets transmis vaut le coût d'instructions supplémentaires lors de l'utilisation des données. En conséquence, les protobufs utilisent un codage qui code les entiers en 1, 2, 3, 4 ou 5 octets, et les plus petits entiers prennent moins d'octets. Une fois le message reçu, cependant, il est décompressé dans un format d'entier de taille fixe plus traditionnel qui est plus facile à utiliser. Ce n'est que pendant la transmission réseau qu'ils utilisent un entier de longueur variable aussi efficace en termes d'espace.
la source
J'aime l'analogie de la maison de Sergey , mais je pense qu'une analogie avec la voiture serait meilleure.
Imaginez des types de variables comme des types de voitures et des personnes comme des données. Lorsque nous recherchons une nouvelle voiture, nous choisissons celle qui correspond le mieux à notre objectif. Voulons-nous une petite voiture intelligente qui ne peut accueillir qu'une ou deux personnes? Ou une limousine pour transporter plus de monde? Les deux ont leurs avantages et leurs inconvénients comme la vitesse et la consommation d'essence (pensez à la vitesse et à l'utilisation de la mémoire).
Si vous avez une limousine et que vous conduisez seul, elle ne rétrécira pas pour ne s'adapter qu'à vous. Pour ce faire, vous devez vendre la voiture (lire: désallouer) et en acheter une nouvelle plus petite pour vous-même.
En poursuivant l'analogie, vous pouvez considérer la mémoire comme un immense parking rempli de voitures, et lorsque vous allez lire, un chauffeur spécialisé formé uniquement pour votre type de voiture va la chercher pour vous. Si votre voiture pouvait changer de type en fonction des personnes à l'intérieur, vous devrez amener toute une série de chauffeurs à chaque fois que vous voudriez obtenir votre voiture, car ils ne sauraient jamais quel type de voiture sera assis sur place.
En d'autres termes, essayer de déterminer la quantité de mémoire que vous devez lire au moment de l'exécution serait extrêmement inefficace et l'emporterait sur le fait que vous pourriez peut-être installer quelques voitures de plus dans votre parking.
la source
Il y a quelques raisons. L'un est la complexité supplémentaire pour la gestion des nombres de taille arbitraire et les performances que cela donne, car le compilateur ne peut plus optimiser en supposant que chaque int fait exactement X octets.
Un deuxième est que stocker des types simples de cette manière signifie qu'ils ont besoin d'un octet supplémentaire pour contenir la longueur. Ainsi, une valeur de 255 ou moins nécessite en fait deux octets dans ce nouveau système, pas un, et dans le pire des cas, vous avez maintenant besoin de 5 octets au lieu de 4. Cela signifie que la performance gagnée en termes de mémoire utilisée est inférieure à ce que vous pourriez penser et dans certains cas marginaux pourrait en fait être une perte nette.
Une troisième raison est que la mémoire de l'ordinateur est généralement adressable en mots et non en octets. (Mais voir la note en bas de page). Les mots sont un multiple d'octets, généralement 4 sur les systèmes 32 bits et 8 sur les systèmes 64 bits. Vous ne pouvez généralement pas lire un octet individuel, vous lisez un mot et extrayez le nième octet de ce mot. Cela signifie à la fois que l'extraction d'octets individuels d'un mot demande un peu plus d'effort que la simple lecture du mot entier et qu'il est très efficace si la mémoire entière est uniformément divisée en morceaux de la taille d'un mot (c'est-à-dire de 4 octets). Parce que, si vous avez des entiers de taille arbitraire flottant autour, vous pourriez vous retrouver avec une partie de l'entier dans un mot, et une autre dans le mot suivant, nécessitant deux lectures pour obtenir l'entier complet.
Note de bas de page: Pour être plus précis, alors que vous avez adressé des octets, la plupart des systèmes ignoraient les octets «inégaux». Par exemple, les adresses 0, 1, 2 et 3 lisent toutes le même mot, 4, 5, 6 et 7 lisent le mot suivant, et ainsi de suite.
Sur une note inédite, c'est aussi pourquoi les systèmes 32 bits avaient un maximum de 4 Go de mémoire. Les registres utilisés pour adresser les emplacements en mémoire sont généralement assez grands pour contenir un mot, c'est-à-dire 4 octets, qui a une valeur maximale de (2 ^ 32) -1 = 4294967295. 4294967296 octets équivaut à 4 Go.
la source
Il existe des objets qui, dans un certain sens, ont une taille variable, dans la bibliothèque standard C ++, comme
std::vector
. Cependant, tous allouent dynamiquement la mémoire supplémentaire dont ils auront besoin. Si vous prenezsizeof(std::vector<int>)
, vous obtiendrez une constante qui n'a rien à voir avec la mémoire gérée par l'objet, et si vous allouez un tableau ou une structure contenantstd::vector<int>
, il réservera cette taille de base plutôt que de mettre le stockage supplémentaire dans le même tableau ou structure . Il y a quelques morceaux de syntaxe C qui prennent en charge quelque chose comme ça, notamment des tableaux et des structures de longueur variable, mais C ++ n'a pas choisi de les prendre en charge.La norme de langage définit la taille de l'objet de cette façon afin que les compilateurs puissent générer un code efficace. Par exemple, si
int
une implémentation fait 4 octets de long sur une implémentation et que vous déclareza
comme un pointeur ou un tableau deint
valeurs, alors sea[i]
traduit par le pseudocode, «déréférencer l'adresse a + 4 × i». Cela peut être fait en temps constant, et c'est une opération tellement courante et importante que de nombreuses architectures de jeu d'instructions, y compris x86 et les machines DEC PDP sur lesquelles C a été initialement développé, peuvent le faire en une seule instruction machine.Les chaînes codées en UTF-8 sont un exemple concret courant de données stockées consécutivement sous forme d'unités de longueur variable. (Cependant, le type sous-jacent d'une chaîne UTF-8 pour le compilateur est toujours
char
et a la largeur 1. Cela permet aux chaînes ASCII d'être interprétées comme UTF-8 valide et à beaucoup de code de bibliothèque tel questrlen()
etstrncpy()
de continuer à fonctionner.) Le codage de tout point de code UTF-8 peut durer de un à quatre octets, et par conséquent, si vous voulez le cinquième point de code UTF-8 dans une chaîne, il peut commencer n'importe où du cinquième octet au dix-septième octet des données. Le seul moyen de le trouver est de scanner à partir du début de la chaîne et de vérifier la taille de chaque point de code. Si vous voulez trouver le cinquième graphème, vous devez également vérifier les classes de caractères. Si vous voulez trouver le millionième caractère UTF-8 dans une chaîne, vous devez exécuter cette boucle un million de fois! Si vous savez que vous devrez souvent travailler avec des index, vous pouvez parcourir la chaîne une fois et en créer un index - ou vous pouvez convertir en un codage à largeur fixe, tel que UCS-4. Trouver le millionième caractère UCS-4 dans une chaîne est juste une question d'ajouter quatre millions à l'adresse du tableau.Une autre complication avec les données de longueur variable est que, lorsque vous les allouez, vous devez soit allouer autant de mémoire que possible, soit réallouer dynamiquement si nécessaire. Allouer pour le pire des cas pourrait être extrêmement coûteux. Si vous avez besoin d'un bloc de mémoire consécutif, la réallocation peut vous forcer à copier toutes les données vers un emplacement différent, mais permettre à la mémoire d'être stockée dans des blocs non consécutifs complique la logique du programme.
Ainsi , il est possible d'avoir bignums de longueur variable au lieu de largeur fixe
short int
,int
,long int
etlong long int
, mais il serait inefficace d'affecter et de les utiliser. De plus, tous les processeurs traditionnels sont conçus pour faire de l'arithmétique sur des registres de largeur fixe, et aucun n'a d'instructions qui fonctionnent directement sur une sorte de bignum de longueur variable. Celles-ci devraient être implémentées dans le logiciel, beaucoup plus lentement.Dans le monde réel, la plupart des programmeurs (mais pas tous) ont décidé que les avantages de l'encodage UTF-8, en particulier la compatibilité, sont importants, et que nous nous soucions si rarement de rien d'autre que de scanner une chaîne de l'avant vers l'arrière ou de copier des blocs de mémoire que les inconvénients de largeur variable sont acceptables. Nous pourrions utiliser des éléments compressés de largeur variable similaires à UTF-8 pour d'autres choses. Mais nous le faisons très rarement, et ils ne sont pas dans la bibliothèque standard.
la source
Principalement en raison des exigences d'alignement.
Selon basic.align / 1 :
Pensez à un bâtiment qui a plusieurs étages et chaque étage a de nombreuses pièces.
Chaque pièce est de votre taille (un espace fixe) capable de contenir N quantité de personnes ou d'objets.
Avec la taille de la pièce connue à l'avance, cela rend le composant structurel du bâtiment bien structuré .
Si les pièces ne sont pas alignées, le squelette du bâtiment ne sera pas bien structuré.
la source
Cela peut être moins. Considérez la fonction:
il compile en code d'assemblage (g ++, x64, détails supprimés)
Ici,
bar
etbaz
finissez par utiliser zéro octet pour représenter.la source
Parce que vous lui avez dit d'en utiliser autant. Lors de l'utilisation d'un
unsigned int
, certaines normes dictent que 4 octets seront utilisés et que la plage disponible pour celui-ci sera de 0 à 4 294 967 295. Si vous deviez utiliser un à launsigned char
place, vous n'utiliseriez probablement que le 1 octet que vous recherchez (selon la norme et C ++ utilise normalement ces normes).S'il n'y avait pas ces normes, vous devriez garder cela à l'esprit: comment le compilateur ou le processeur est-il censé savoir qu'il n'utilise qu'un octet au lieu de 4? Plus tard dans votre programme, vous pourriez ajouter ou multiplier cette valeur, ce qui nécessiterait plus d'espace. Chaque fois que vous faites une allocation de mémoire, le système d'exploitation doit trouver, mapper et vous donner cet espace (potentiellement en échangeant de la mémoire vers la RAM virtuelle); cela peut prendre du temps. Si vous allouez la mémoire au préalable, vous n'aurez pas à attendre qu'une autre allocation soit terminée.
Quant à la raison pour laquelle nous utilisons 8 bits par octet, vous pouvez jeter un oeil à ceci: Quelle est l'histoire de pourquoi les octets sont huit bits?
Sur une note latérale, vous pouvez permettre à l'entier de déborder; mais si vous utilisez un entier signé, les normes C \ C ++ indiquent que les débordements d'entiers entraînent un comportement non défini. Débordement d'entier
la source
Quelque chose de simple que la plupart des réponses semblent manquer:
car il convient aux objectifs de conception de C ++.
Être capable de calculer la taille d'un type au moment de la compilation permet au compilateur et au programmeur de faire un grand nombre d'hypothèses simplificatrices, ce qui apporte de nombreux avantages, notamment en termes de performances. Bien sûr, les types de taille fixe ont des pièges concomitants tels que le débordement d'entiers. C'est pourquoi différents langages prennent des décisions de conception différentes. (Par exemple, les entiers Python sont essentiellement de taille variable.)
La principale raison pour laquelle C ++ se penche si fortement sur les types de taille fixe est probablement son objectif de compatibilité C. Cependant, étant donné que C ++ est un langage de type statique qui tente de générer du code très efficace et évite d'ajouter des éléments non explicitement spécifiés par le programmeur, les types de taille fixe ont encore beaucoup de sens.
Alors pourquoi C a-t-il opté pour les types de taille fixe en premier lieu? Facile. Il a été conçu pour écrire des systèmes d'exploitation, des logiciels serveur et des utilitaires des années 70; les choses qui ont fourni une infrastructure (comme la gestion de la mémoire) pour d'autres logiciels. À un niveau aussi bas, les performances sont essentielles, tout comme le compilateur fait précisément ce à quoi vous lui demandez.
la source
Changer la taille d'une variable nécessiterait une réallocation et cela ne vaut généralement pas les cycles CPU supplémentaires par rapport au gaspillage de quelques octets supplémentaires de mémoire.
Les variables locales vont sur une pile qui est très rapide à manipuler lorsque ces variables ne changent pas de taille. Si vous décidez d'étendre la taille d'une variable de 1 octet à 2 octets, vous devez déplacer tout ce qui se trouve sur la pile d'un octet pour lui faire cet espace. Cela peut potentiellement coûter beaucoup de cycles CPU en fonction du nombre de choses à déplacer.
Une autre façon de le faire est de faire de chaque variable un pointeur vers un emplacement de tas, mais vous gaspilleriez encore plus de cycles CPU et de mémoire de cette façon, en fait. Les pointeurs font 4 octets (adressage 32 bits) ou 8 octets (adressage 64 bits), vous utilisez donc déjà 4 ou 8 pour le pointeur, puis la taille réelle des données sur le tas. Il y a toujours un coût de réaffectation dans ce cas. Si vous avez besoin de réallouer des données de tas, vous pourriez avoir de la chance et avoir de la place pour les étendre en ligne, mais parfois vous devez les déplacer ailleurs sur le tas pour avoir le bloc de mémoire contigu de la taille souhaitée.
Il est toujours plus rapide de décider à l'avance de la quantité de mémoire à utiliser. Si vous pouvez éviter le dimensionnement dynamique, vous gagnez en performances. La perte de mémoire vaut généralement le gain de performances. C'est pourquoi les ordinateurs ont des tonnes de mémoire. :)
la source
Le compilateur est autorisé à apporter de nombreuses modifications à votre code, tant que les choses fonctionnent encore (la règle "tel quel").
Il serait possible d'utiliser une instruction de déplacement littérale de 8 bits au lieu de la plus longue (32/64 bits) requise pour déplacer un plein
int
. Cependant, vous auriez besoin de deux instructions pour terminer le chargement, car vous devriez d'abord mettre le registre à zéro avant de faire le chargement.Il est simplement plus efficace (du moins selon les principaux compilateurs) de traiter la valeur en 32 bits. En fait, je n'ai pas encore vu de compilateur x86 / x86_64 qui effectuerait un chargement 8 bits sans assemblage en ligne.
Cependant, les choses sont différentes pour le 64 bits. Lors de la conception des extensions précédentes (de 16 à 32 bits) de leurs processeurs, Intel a commis une erreur. Voici une bonne représentation de ce à quoi ils ressemblent. Le principal à retenir ici est que lorsque vous écrivez à AL ou AH, l'autre n'est pas affecté (assez juste, c'était le point et cela avait du sens à l'époque). Mais cela devient intéressant quand ils l'ont étendu à 32 bits. Si vous écrivez les bits inférieurs (AL, AH ou AX), rien ne se passe avec les 16 bits supérieurs d'EAX, ce qui signifie que si vous voulez promouvoir a
char
en aint
, vous devez d'abord effacer cette mémoire, mais vous n'avez aucun moyen de en utilisant uniquement ces 16 premiers bits, ce qui rend cette "fonctionnalité" plus pénible qu'autre chose.Désormais, avec 64 bits, AMD a fait un bien meilleur travail. Si vous touchez quelque chose dans les 32 bits inférieurs, les 32 bits supérieurs sont simplement mis à 0. Cela conduit à des optimisations réelles que vous pouvez voir dans ce godbolt . Vous pouvez voir que le chargement de quelque chose de 8 bits ou 32 bits se fait de la même manière, mais lorsque vous utilisez des variables de 64 bits, le compilateur utilise une instruction différente en fonction de la taille réelle de votre littéral.
Vous pouvez donc voir ici, les compilateurs peuvent totalement changer la taille réelle de votre variable à l'intérieur du CPU si cela produisait le même résultat, mais cela n'a aucun sens de le faire pour les types plus petits.
la source