Est-ce une bonne pratique d'utiliser des types de données plus petits pour les variables pour économiser de la mémoire?

32

Lorsque j'ai appris le langage C ++ pour la première fois, j'ai appris qu'en plus de int, float, etc., des versions plus ou moins grandes de ces types de données existaient dans le langage. Par exemple, je pourrais appeler une variable x

int x;
or 
short int x;

La principale différence étant que short int prend 2 octets de mémoire tandis que int prend 4 octets, et short int a une valeur moindre, mais nous pourrions également l'appeler pour la rendre encore plus petite:

int x;
short int x;
unsigned short int x;

ce qui est encore plus restrictif.

Ma question ici est de savoir si c'est une bonne pratique d'utiliser des types de données distincts en fonction des valeurs que votre variable prend dans le programme. Est-ce une bonne idée de toujours déclarer des variables en fonction de ces types de données?

Bugster
la source
3
connaissez-vous le modèle de conception Flyweight ? "un objet qui minimise l'utilisation de la mémoire en partageant autant de données que possible avec d'autres objets similaires; c'est un moyen d'utiliser des objets en grand nombre lorsqu'une simple représentation répétée utiliserait une quantité de mémoire inacceptable ..."
gnat
5
Avec les paramètres standard du compilateur de compression / alignement, les variables seront de toute façon alignées sur des limites de 4 octets, donc il pourrait ne pas y avoir de différence du tout.
nikie
36
Cas classique d'optimisation prématurée.
scarfridge
1
@nikie - ils peuvent être alignés sur une limite de 4 octets sur un processeur x86 mais ce n'est pas vrai en général. MSP430 place char sur n'importe quelle adresse d'octet et tout le reste sur une adresse d'octet pair. Je pense que l'AVR-32 et l'ARM Cortex-M sont les mêmes.
uɐɪ
3
La deuxième partie de votre question implique que l'ajout d'une unsignedmanière ou d'une autre fait qu'un entier occupe moins d'espace, ce qui est bien sûr faux. Il aura le même nombre de valeurs représentables discrètes (donner ou prendre 1 selon la façon dont le signe est représenté), mais simplement décalé exclusivement dans le positif.
underscore_d

Réponses:

41

La plupart du temps, le coût de l'espace est négligeable et vous ne devriez pas vous en soucier, mais vous devez vous soucier des informations supplémentaires que vous donnez en déclarant un type. Par exemple, si vous:

unsigned int salary;

Vous donnez une information utile à un autre développeur: le salaire ne peut pas être négatif.

La différence entre court, int, long va rarement causer des problèmes d'espace dans votre application. Il est plus probable que vous fassiez accidentellement l'hypothèse erronée qu'un nombre rentre toujours dans un type de données. Il est probablement plus sûr de toujours utiliser int sauf si vous êtes sûr à 100% que vos chiffres seront toujours très petits. Même dans ce cas, il est peu probable que vous économisiez une quantité notable d'espace.

Oleksi
la source
5
C'est vrai que cela va rarement causer des problèmes de nos jours, mais si vous concevez une bibliothèque ou une classe qu'un autre développeur utilisera, eh bien c'est une autre question. Peut-être qu'ils auront besoin de stockage pour un million de ces objets, auquel cas la différence est grande - 4 Mo par rapport à 2 Mo uniquement pour ce seul champ.
dodgy_coder
30
L'utilisation unsigneddans ce cas est une mauvaise idée: non seulement le salaire ne peut pas être négatif, mais la différence entre deux salaires ne peut pas non plus être négative. (En général, utiliser unsigned pour autre chose que le bit-twiddling et avoir un comportement défini sur le débordement est une mauvaise idée.)
zvrba
16
@zvrba: La différence entre deux salaires n'est pas en soi un salaire et il est donc légitime d'utiliser un type différent qui est signé.
JeremyP
12
@JeremyP Oui, mais si vous utilisez C (et il semble que cela soit également vrai en C ++), la soustraction d'entier non signé entraîne un entier non signé , qui ne peut pas être négatif. Elle peut devenir la bonne valeur si vous la transformez en un entier signé, mais le résultat du calcul est un entier non signé. Voir aussi cette réponse pour plus de bizarreries de calcul signées / non signées - c'est pourquoi vous ne devriez jamais utiliser de variables non signées à moins que vous ne tourniez vraiment des bits.
Tacroy
5
@zvrba: La différence est une quantité monétaire mais pas un salaire. Maintenant, vous pourriez faire valoir qu'un salaire est également une quantité monétaire (limité à des nombres positifs et à 0 en validant l'entrée, ce que la plupart des gens feraient), mais la différence entre deux salaires n'est pas en soi un salaire.
JeremyP
29

L'OP n'a rien dit sur le type de système pour lequel ils écrivent des programmes, mais je suppose que l'OP pensait à un PC typique avec des Go de mémoire puisque C ++ est mentionné. Comme le dit l'un des commentaires, même avec ce type de mémoire, si vous avez plusieurs millions d'éléments d'un même type - comme un tableau - alors la taille de la variable peut faire une différence.

Si vous entrez dans le monde des systèmes embarqués - ce qui n'est pas vraiment hors de portée de la question, puisque l'OP ne le limite pas aux PC - alors la taille des types de données est très importante. Je viens de terminer un projet rapide sur un microcontrôleur 8 bits qui n'a que 8K mots de mémoire de programme et 368 octets de RAM. Là, évidemment, chaque octet compte. On n'utilise jamais une variable plus grande que nécessaire (à la fois du point de vue de l'espace et de la taille du code - les processeurs 8 bits utilisent beaucoup d'instructions pour manipuler les données 16 et 32 ​​bits). Pourquoi utiliser un CPU avec des ressources aussi limitées? En grandes quantités, elles peuvent coûter aussi peu qu'un quart.

Je fais actuellement un autre projet intégré avec un microcontrôleur basé sur MIPS 32 bits qui a 512K octets de flash et 128K octets de RAM (et coûte environ 6 $ en quantité). Comme avec un PC, la taille des données "naturelles" est de 32 bits. Maintenant, il devient plus efficace, au niveau du code, d'utiliser des entiers pour la plupart des variables au lieu des caractères ou des courts-circuits. Mais encore une fois, tout type de tableau ou de structure doit être considéré si des types de données plus petits sont garantis. Contrairement à compilateurs pour les grands systèmes, il est plus probable des variables dans une structure seront être emballés sur un système embarqué. Je prends soin de toujours essayer de mettre toutes les variables 32 bits en premier, puis 16 bits, puis 8 bits pour éviter tout "trou".

tcrosley
la source
10
+1 pour le fait que différentes règles s'appliquent aux systèmes embarqués. Le fait que C ++ soit mentionné ne signifie pas que la cible est un PC. Un de mes projets récents a été écrit en C ++ sur un processeur avec 32k de RAM et 256K de Flash.
uɐɪ
13

La réponse dépend de votre système. En règle générale, voici les avantages et les inconvénients de l'utilisation de types plus petits:

Avantages

  • Les types plus petits utilisent moins de mémoire sur la plupart des systèmes.
  • Les types plus petits permettent des calculs plus rapides sur certains systèmes. Particulièrement vrai pour float vs double sur de nombreux systèmes. Et les types int plus petits donnent également un code beaucoup plus rapide sur les processeurs 8 ou 16 bits.

Désavantages

  • De nombreux processeurs ont des exigences d'alignement. Certains accèdent aux données alignées plus rapidement que non alignées. Certains doivent avoir aligné les données pour pouvoir même y accéder. Les types entiers plus grands équivalent à une unité alignée, ils ne sont donc probablement pas désalignés. Cela signifie que le compilateur peut être forcé de mettre vos petits entiers en plus grands. Et si les types plus petits font partie d'une structure plus grande, vous pouvez obtenir divers octets de remplissage insérés silencieusement n'importe où dans la structure par le compilateur, pour corriger l'alignement.
  • Conversions implicites dangereuses. C et C ++ ont plusieurs règles obscures et dangereuses sur la façon dont les variables sont promues en plus grandes, implicitement sans transtypage. Il existe deux ensembles de règles de conversion implicites entrelacées, appelées «règles de promotion des nombres entiers» et «conversions arithmétiques habituelles». En savoir plus à leur sujet ici . Ces règles sont l'une des causes les plus courantes de bogues en C et C ++. Vous pouvez éviter beaucoup de problèmes en utilisant simplement le même type entier dans tout le programme.

Mon conseil est d'aimer ça:

system                             int types

small/low level embedded system    stdint.h with smaller types
32-bit embedded system             stdint.h, stick to int32_t and uint32_t.
32-bit desktop system              Only use (unsigned) int and long long.
64-bit system                      Only use (unsigned) int and long long.

Alternativement, vous pouvez utiliser le int_leastn_tou int_fastn_tde stdint.h, où le n est le nombre 8, 16, 32 ou 64. int_leastn_ttype signifie "Je veux que ce soit au moins n octets mais je me fiche que le compilateur l'alloue comme un type plus grand pour s'adapter à l'alignement ".

int_fastn_t signifie "Je veux que ce soit long de n octets, mais si cela rend mon code plus rapide, le compilateur doit utiliser un type plus grand que celui spécifié".

Généralement, les différents types de stdint.h sont une bien meilleure pratique que plain intetc, car ils sont portables. L'intention intétait de ne pas lui donner une largeur spécifiée uniquement pour le rendre portable. Mais en réalité, il est difficile de porter car vous ne savez jamais quelle sera sa taille sur un système spécifique.


la source
Repérer sur l'alignement. Dans mon projet actuel, l'utilisation gratuite d'uint8_t sur un MSP430 16 bits a fait planter le MCU de façon mystérieuse (un accès très probablement mal aligné s'est produit quelque part, peut-être la faute de GCC, peut-être pas) - le simple remplacement de tous uint8_t par `` non signé '' a éliminé les plantages. L'utilisation de types 8 bits sur des arcs> 8 bits si elle n'est pas fatale est au moins inefficace: le compilateur génère des instructions supplémentaires «et reg, 0xff». Utilisez 'int / unsigned' pour la portabilité et libérez le compilateur de contraintes supplémentaires.
alexei
11

Selon le fonctionnement du système d'exploitation spécifique, vous vous attendez généralement à ce que la mémoire soit allouée non optimisée de sorte que lorsque vous appelez un octet, ou un mot ou un autre petit type de données à allouer, la valeur occupe un registre entier tout cela est très posséder. Le fonctionnement de votre compilateur ou interprète pour interpréter ceci est cependant autre chose, donc si vous compilez un programme en C # par exemple, la valeur pourrait occuper physiquement un registre pour elle-même, mais la valeur sera vérifiée pour vous assurer que vous ne le faites pas essayez de stocker une valeur qui dépassera les limites du type de données voulu.

En termes de performances, et si vous êtes vraiment pédant à propos de telles choses, il est probablement plus rapide d'utiliser simplement le type de données qui correspond le mieux à la taille du registre cible, mais vous passez à côté de tout ce joli sucre syntaxique qui rend le travail avec les variables si facile .

Comment cela vous aide-t-il? Eh bien, c'est vraiment à vous de décider pour quel type de situation vous codez. Pour presque tous les programmes que j'ai jamais écrits, il suffit de faire simplement confiance à votre compilateur pour optimiser les choses et utiliser le type de données qui vous est le plus utile. Si vous avez besoin d'une grande précision, utilisez les types de données à virgule flottante plus grands. Si vous ne travaillez qu'avec des valeurs positives, vous pouvez probablement utiliser un entier non signé, mais pour la plupart, il suffit d'utiliser le type de données int.

Si toutefois vous avez des exigences de données très strictes, telles que l'écriture d'un protocole de communication ou une sorte d'algorithme de chiffrement, l'utilisation de types de données à plage vérifiée peut s'avérer très utile, en particulier si vous essayez d'éviter les problèmes liés aux dépassements / sous-dépassements de données ou des valeurs de données non valides.

La seule autre raison à laquelle je peux penser du haut de ma tête pour utiliser des types de données spécifiques est lorsque vous essayez de communiquer l'intention dans votre code. Si vous utilisez un raccourci par exemple, vous dites à d'autres développeurs que vous autorisez les nombres positifs et négatifs dans une très petite plage de valeurs.

S.Robins
la source
6

Comme l'a expliqué Scarfridge , il s'agit d'un

Cas classique d' optimisation prématurée .

Tenter d'optimiser l'utilisation de la mémoire peut avoir un impact sur d'autres domaines de performances, et les règles d'or de l'optimisation sont les suivantes:

La première règle d'optimisation de programme: ne le faites pas .

La deuxième règle de l'optimisation des programmes (pour les experts uniquement!): Ne le faites pas encore . "

- Michael A. Jackson

Afin de savoir si le moment est venu d'optimiser, il faut des analyses comparatives et des tests. Vous devez savoir où votre code est inefficace, afin de pouvoir cibler vos optimisations.

Afin de déterminer si la version optimisée du code est réellement meilleure que l'implémentation naïve à un moment donné, vous devez les comparer côte à côte avec les mêmes données.

N'oubliez pas que le fait qu'une implémentation donnée soit plus efficace sur la génération actuelle de CPU ne signifie pas qu'elle le sera toujours . Ma réponse à la question La micro-optimisation est-elle importante lors du codage? détaille un exemple d'expérience personnelle où une optimisation obsolète a entraîné un ralentissement de l'ordre de grandeur.

Sur de nombreux processeurs, les accès à la mémoire non alignés sont nettement plus coûteux que les accès à la mémoire alignés. Emballer quelques courts métrages dans votre structure peut simplement signifier que votre programme doit effectuer une opération de pack / unpack chaque fois que vous touchez l'une ou l'autre valeur.

Pour cette raison, les compilateurs modernes ignorent vos suggestions. Comme le commente Nikie :

Avec les paramètres standard du compilateur de compression / alignement, les variables seront de toute façon alignées sur des limites de 4 octets, donc il pourrait ne pas y avoir de différence du tout.

Devinez votre compilateur à vos risques et périls.

Il y a une place pour de telles optimisations, lorsque vous travaillez avec des ensembles de données de téraoctets ou des microcontrôleurs intégrés, mais pour la plupart d'entre nous, ce n'est pas vraiment un problème.

Mark Booth
la source
3

La principale différence étant que short int prend 2 octets de mémoire tandis que int prend 4 octets, et short int a une valeur moindre, mais nous pourrions également l'appeler pour la rendre encore plus petite:

Ceci est une erreur. Vous ne pouvez pas faire d'hypothèses sur le nombre d'octets que chaque type contient, à part charêtre un octet et au moins 8 bits par octet, la taille de chaque type étant supérieure ou égale à la précédente.

Les avantages en termes de performances sont incroyablement minuscules pour les variables de pile - ils seront probablement alignés / remplis de toute façon.

Pour cette raison, shortet longn'ont pratiquement aucune utilité de nos jours, et vous êtes presque toujours mieux d'utiliser int.


Bien sûr, il y a aussi stdint.hce qui est parfaitement bien à utiliser quand intil ne le coupe pas. Si jamais vous allouez d'énormes tableaux d'entiers / structures, alors un intX_test logique car vous pouvez être efficace et vous fier à la taille du type. Ce n'est pas du tout prématuré car vous pouvez économiser des mégaoctets de mémoire.

Pubby
la source
1
En fait, avec l'avènement des environnements 64 bits, il longpeut être différent de int. Si votre compilateur est LP64, int32 bits et long64 bits, vous constaterez que ints peut toujours être aligné sur 4 octets (mon compilateur le fait, par exemple).
JeremyP
1
@ JeremyP Ouais, ai-je dit le contraire ou quelque chose?
Pubby
Votre dernière phrase qui prétend courte et longue n'a pratiquement aucune utilité. Long a certainement une utilité, ne serait-ce que comme type de base deint64_t
JeremyP
@ JeremyP: Vous pouvez vivre très bien avec int et longtemps.
gnasher729
@ gnasher729: Qu'utilisez-vous si vous avez besoin d'une variable pouvant contenir des valeurs supérieures à 65 000, mais jamais autant qu'un milliard? int32_t,, int_fast32_tet longsont toutes de bonnes options, long longest juste un gaspillage et intnon portable.
Ben Voigt
3

Ce sera d'une sorte de POO et / ou de point de vue entreprise / application et pourrait ne pas être applicable dans certains domaines / domaines, mais je voudrais en quelque sorte évoquer le concept d' obsession primitive .

C'est une bonne idée d'utiliser différents types de données pour différents types d'informations dans votre application. Cependant, ce n'est probablement PAS une bonne idée d'utiliser les types intégrés pour cela, sauf si vous avez de sérieux problèmes de performances (qui ont été mesurés et vérifiés, etc.).

Si nous voulons modéliser les températures en Kelvin dans notre application, nous POUVONS utiliser un ushortou uintou quelque chose de similaire pour indiquer que "la notion de degrés négatifs Kelvin est absurde et une erreur de logique de domaine". L'idée derrière cela est saine, mais vous n'allez pas jusqu'au bout. Ce que nous avons réalisé, c'est que nous ne pouvons pas avoir de valeurs négatives, donc c'est pratique si nous pouvons obtenir le compilateur pour nous assurer que personne n'attribue une valeur négative à une température Kelvin. Il est également vrai que vous ne pouvez pas effectuer d'opérations au niveau du bit sur les températures. Et vous ne pouvez pas ajouter une mesure de poids (kg) à une température (K). Mais si vous modélisez à la fois la température et la masse en uints, nous pouvons le faire.

L'utilisation de types intégrés pour modéliser nos entités DOMAIN est susceptible de conduire à un code désordonné, à des vérifications manquées et à des invariants cassés. Même si un type capture QUELQUE partie de l'entité (ne peut pas être négatif), il en manquera forcément d'autres (ne peut pas être utilisé dans des expressions arithmétiques arbitraires, ne peut pas être traité comme un tableau de bits, etc.)

La solution est de définir de nouveaux types qui encapsulent les invariants. De cette façon, vous pouvez vous assurer que l'argent est de l'argent et que les distances sont des distances, et vous ne pouvez pas les additionner, et vous ne pouvez pas créer une distance négative, mais vous POUVEZ créer un montant d'argent (ou une dette) négatif. Bien sûr, ces types utiliseront les types intégrés en interne, mais cela est caché aux clients. Relativement à votre question sur les performances / la consommation de mémoire, ce genre de chose peut vous permettre de changer la façon dont les choses sont stockées en interne sans changer l'interface de vos fonctions qui opèrent sur vos entités de domaine, si vous découvrez que putain, a shortest tout simplement trop putain grand.

Sara
la source
1

Oui bien sûr. C'est une bonne idée d'utiliser uint_least8_tpour les dictionnaires, les énormes tableaux de constantes, les tampons, etc. Il est préférable d'utiliser uint_fast8_tà des fins de traitement.

uint8_least_t(stockage) -> uint8_fast_t(traitement) -> uint8_least_t(stockage).

Par exemple, vous prenez un symbole de 8 bits source, des codes de 16 bits dictionarieset quelque 32 bits constants. Que vous traitez avec eux des opérations 10-15 bits et que vous sortez 8 bits destination.

Imaginons que vous devez traiter 2 gigaoctets de source. Le nombre d'opérations sur les bits est énorme. Vous recevrez un excellent bonus de performance si vous passez à des types rapides pendant le traitement. Les types rapides peuvent être différents pour chaque famille de CPU. Vous pouvez inclure stdint.het de l' utilisation uint_fast8_t, uint_fast16_t, uint_fast32_t, etc.

Vous pouvez utiliser uint_least8_tau lieu de uint8_tpour la portabilité. Mais personne ne sait réellement quel processeur moderne utilisera cette fonctionnalité. La machine VAC est une pièce de musée. Alors peut-être que c'est une exagération.

puchu
la source
1
Bien que vous puissiez avoir un point avec les types de données que vous avez répertoriés, vous devez expliquer pourquoi ils sont meilleurs plutôt que de simplement déclarer que c'est le cas. Pour les gens comme moi qui ne connaissent pas ces types de données, j'ai dû les rechercher sur Google pour comprendre de quoi vous parlez.
Peter M