N'est-ce pas une surcharge de mémoire?
Oui Non peut-être?
C'est une question délicate, car imaginez la plage d'adressage de la mémoire sur la machine et un logiciel qui doit constamment garder une trace de l'endroit où les choses sont en mémoire d'une manière qui ne peut pas être liée à la pile.
Par exemple, imaginez un lecteur de musique où le fichier de musique est chargé sur un bouton poussé par l'utilisateur et déchargé de la mémoire volatile lorsque l'utilisateur essaie de charger un autre fichier de musique.
Comment pouvons-nous savoir où sont stockées les données audio? Nous avons besoin d'une adresse mémoire pour cela. Le programme doit non seulement garder une trace du morceau de données audio en mémoire, mais aussi où il se trouve en mémoire. Nous devons donc conserver une adresse mémoire (c'est-à-dire un pointeur). Et la taille du stockage requis pour l'adresse mémoire va correspondre à la plage d'adressage de la machine (ex: pointeur 64 bits pour une plage d'adressage 64 bits).
C'est donc un peu "oui", cela nécessite un stockage pour garder une trace d'une adresse mémoire, mais ce n'est pas comme si nous pouvions l'éviter pour une mémoire allouée dynamiquement de ce type.
Comment est-ce compensé?
En parlant uniquement de la taille d'un pointeur lui-même, vous pouvez éviter le coût dans certains cas en utilisant la pile, par exemple. Dans ce cas, les compilateurs peuvent générer des instructions qui codent en dur l'adresse de mémoire relative, évitant ainsi le coût d'un pointeur. Pourtant, cela vous rend vulnérable aux débordements de pile si vous le faites pour de grandes allocations de taille variable, et a également tendance à être impossible (sinon carrément impossible) à faire pour une série complexe de branches pilotées par l'entrée utilisateur (comme dans l'exemple audio au dessus de).
Une autre façon consiste à utiliser des structures de données plus contiguës. Par exemple, une séquence basée sur un tableau peut être utilisée à la place d'une liste à double liaison qui nécessite deux pointeurs par nœud. Nous pouvons également utiliser un hybride de ces deux comme une liste déroulée qui ne stocke que des pointeurs entre chaque groupe contigu de N éléments.
Les pointeurs sont-ils utilisés dans des applications à faible mémoire critique?
Oui, très souvent, car de nombreuses applications critiques pour les performances sont écrites en C ou C ++ qui sont dominées par l'utilisation du pointeur (elles peuvent être derrière un pointeur intelligent ou un conteneur comme std::vector
ou std::string
, mais la mécanique sous-jacente se résume à un pointeur utilisé pour garder l'adresse d'un bloc de mémoire dynamique).
Revenons maintenant à cette question:
Comment est-ce compensé? (Deuxième partie)
Les pointeurs sont généralement très bon marché, sauf si vous en stockez comme un million (ce qui est toujours un maigre * 8 mégaoctets sur une machine 64 bits).
* Notez que Ben a souligné qu'un "maigre" 8 Mo est toujours la taille du cache L3. Ici, j'ai utilisé "maigre" davantage dans le sens de l'utilisation totale de la DRAM et de la taille relative typique des morceaux de mémoire vers laquelle une utilisation saine des pointeurs indiquera.
Les pointeurs qui coûtent cher ne sont pas les pointeurs eux-mêmes, mais:
Allocation dynamique de mémoire. L'allocation dynamique de mémoire a tendance à être coûteuse car elle doit passer par une structure de données sous-jacente (ex: alloué copain ou dalle). Même si ceux-ci sont souvent optimisés à mort, ils sont polyvalents et conçus pour gérer des blocs de taille variable qui nécessitent qu'ils effectuent au moins un peu de travail ressemblant à une "recherche" (bien que léger et peut-être même à temps constant) pour trouver un ensemble gratuit de pages contiguës en mémoire.
Accès mémoire. Cela a tendance à être le plus gros frais généraux à s'inquiéter. Chaque fois que nous accédons à la mémoire allouée dynamiquement pour la première fois, il y a une erreur de page obligatoire ainsi que des échecs de cache en déplaçant la mémoire dans la hiérarchie de la mémoire et dans un registre.
Accès à la mémoire
L'accès à la mémoire est l'un des aspects les plus critiques des performances au-delà des algorithmes. De nombreux domaines critiques pour les performances, tels que les moteurs de jeux AAA, consacrent une grande partie de leur énergie à des optimisations orientées données qui se résument à des schémas d'accès à la mémoire et des dispositions plus efficaces.
L'une des plus grandes difficultés de performance des langages de niveau supérieur qui souhaitent allouer chaque type défini par l'utilisateur séparément via un garbage collector, par exemple, est qu'ils peuvent fragmenter un peu la mémoire. Cela peut être particulièrement vrai si tous les objets ne sont pas alloués en même temps.
Dans ces cas, si vous stockez une liste d'un million d'instances d'un type d'objet défini par l'utilisateur, l'accès à ces instances séquentiellement dans une boucle peut être assez lent car il est analogue à une liste d'un million de pointeurs qui pointent vers des régions de mémoire disparates. Dans ces cas, l'architecture souhaite récupérer la mémoire sous des niveaux supérieurs, plus lents et plus grands de la hiérarchie dans de gros morceaux alignés dans l'espoir que les données environnantes dans ces morceaux seront accessibles avant l'expulsion. Lorsque chaque objet dans une telle liste est alloué séparément, nous finissons souvent par le payer avec des échecs de cache lorsque chaque itération suivante peut être chargée à partir d'une zone complètement différente en mémoire sans qu'aucun objet adjacent ne soit accessible avant l'expulsion.
Beaucoup de compilateurs pour de telles langues font un très bon travail ces jours-ci dans la sélection des instructions et l'allocation des registres, mais le manque de contrôle plus direct sur la gestion de la mémoire ici peut être mortel (bien que souvent moins sujet aux erreurs) et toujours créer des langues comme C et C ++ assez populaires.
Optimisation indirecte de l'accès au pointeur
Dans les scénarios les plus critiques pour les performances, les applications utilisent souvent des pools de mémoire qui regroupent la mémoire à partir de blocs contigus pour améliorer la localité de référence. Dans de tels cas, même une structure liée comme un arbre ou une liste liée peut être rendue compatible avec le cache à condition que la disposition de la mémoire de ses nœuds soit de nature contiguë. Cela rend effectivement le déréférencement de pointeur moins cher, quoique indirectement en améliorant la localité de référence impliquée lors du déréférencement.
Chasser les pointeurs autour
Supposons que nous ayons une liste à liaison unique comme:
Foo->Bar->Baz->null
Le problème est que si nous allouons tous ces nœuds séparément par rapport à un allocateur à usage général (et éventuellement pas tous en même temps), la mémoire réelle peut être dispersée un peu comme ceci (diagramme simplifié):
Lorsque nous commençons à rechercher des pointeurs et à accéder au Foo
nœud, nous commençons par un échec obligatoire (et éventuellement un défaut de page) en déplaçant un morceau de sa région de mémoire des régions de mémoire plus lentes vers des régions de mémoire plus rapides, comme ceci:
Cela nous amène à mettre en cache (éventuellement aussi à la page) une région de mémoire uniquement pour accéder à une partie de celle-ci et à expulser le reste pendant que nous poursuivons des pointeurs autour de cette liste. En prenant le contrôle de l'allocateur de mémoire, cependant, nous pouvons allouer une telle liste de manière contiguë comme ceci:
... et ainsi améliorer considérablement la vitesse à laquelle nous pouvons déréférencer ces pointeurs et traiter leurs pointes. Ainsi, bien que très indirect, nous pouvons accélérer l'accès au pointeur de cette façon. Bien sûr, si nous les stockions simplement de manière contiguë dans un tableau, nous n'aurions pas ce problème en premier lieu, mais l'allocateur de mémoire ici nous donnant un contrôle explicite sur la disposition de la mémoire peut sauver le jour où une structure liée est requise.
* Remarque: il s'agit d'un diagramme et d'une discussion très simplifiés sur la hiérarchie de la mémoire et la localité de référence, mais j'espère que cela convient au niveau de la question.