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:
Les registres à usage général 32 bits EAX, EBX, ECX, EDX, ESI, EDI, EBP et ESP sont fournis pour contenir les éléments suivants:
• Opérandes pour les opérations logiques et arithmétiques
• Opérandes pour les calculs d'adresses
• Pointeurs de mémoire
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:
int square(int num) {
return num * num;
}
compile (avec GCC 8.1 et -fomit-frame-pointer -O3
par souci de simplicité) pour:
square(int):
imul edi, edi
mov eax, edi
ret
ça signifie:
- le
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 à convertir
- la multiplication est une seule instruction (
imul
), qui est très rapide
- retourner le résultat consiste simplement à le copier dans un autre registre (l'appelant s'attend à ce que le résultat soit mis dans EAX)
Edit: 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
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 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 du uint32_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).
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.