web-dev-qa-db-fra.com

L'utilisation de variables de pointeur n'est-elle pas une surcharge de mémoire?

Dans des langages comme C et C++, tout en utilisant des pointeurs vers des variables, nous avons besoin d'un emplacement mémoire supplémentaire pour stocker cette adresse. N'est-ce pas une surcharge de mémoire? Comment est-ce compensé? Les pointeurs sont-ils utilisés dans des applications à faible mémoire critique?

28
Sudip Bhandari

En fait, la surcharge ne réside pas vraiment dans les 4 ou 8 octets supplémentaires nécessaires pour stocker le pointeur. La plupart du temps, des pointeurs sont utilisés pour allocation dynamique de mémoire, ce qui signifie que nous invoquons une fonction pour allouer un bloc de mémoire, et cette fonction nous renvoie un pointeur qui pointe vers ce bloc de mémoire. Ce nouveau bloc représente en soi un surcoût considérable.

Maintenant, vous n'avez pas devez vous engager dans l'allocation de mémoire pour utiliser un pointeur: vous pouvez avoir un tableau de int déclaré statiquement ou sur la pile, et vous pouvez utiliser un pointeur au lieu d'un index pour visiter les ints, et tout cela est très agréable et simple et efficace. Aucune allocation de mémoire nécessaire, et le pointeur occupera généralement exactement autant d'espace en mémoire qu'un index entier.

De plus, comme Joshua Taylor nous le rappelle dans un commentaire, les pointeurs sont utilisés pour transmettre quelque chose par référence. Par exemple, struct foo f; init_foo(&f); allouerait f sur la pile, puis appellerait init_foo() avec un pointeur sur cette struct. C'est très courant. (Faites juste attention à ne pas passer ces pointeurs "vers le haut".) En C++, vous pourriez voir cela se faire avec une "référence" (foo&) au lieu d'un pointeur, mais les références ne sont que des pointeurs que vous ne pouvez pas modifier, et elles occupent la même quantité de mémoire.

Mais la principale raison pour laquelle les pointeurs sont utilisés est pour l'allocation dynamique de mémoire, et cela est fait afin de résoudre des problèmes qui ne pourraient pas être résolus autrement. Voici un exemple simpliste: imaginez que vous souhaitez lire l'intégralité du contenu d'un fichier. Où allez-vous les stocker? Si vous essayez avec un tampon de taille fixe, vous ne pourrez lire que les fichiers qui ne sont pas plus longs que ce tampon. Mais en utilisant l'allocation de mémoire, vous pouvez allouer autant de mémoire que nécessaire pour lire le fichier, puis procéder à sa lecture.

De plus, C++ est un langage orienté objet, et certains aspects de OOP comme abstraction ne sont réalisables qu'en utilisant des pointeurs. Même les langages comme Java et C # font extensif l'utilisation de pointeurs, ils ne vous permettent tout simplement pas de manipuler directement les pointeurs, afin de vous empêcher de faire des choses dangereuses avec eux, mais quand même, ces langages commencez à avoir un sens une fois que vous avez réalisé que dans les coulisses tout se fait à l'aide de pointeurs.

Ainsi, les pointeurs ne sont pas uniquement utilisés dans les applications à temps critique et à faible mémoire, ils sont utilisés partout.

33
Mike Nakis

N'est-ce pas une surcharge de mémoire?

Bien sûr, une adresse supplémentaire (généralement 4/8 octets selon le processeur).

Comment est-ce compensé?

Ce n'est pas. Si vous avez besoin de l'indirection nécessaire pour les pointeurs, vous devez payer pour cela.

Les pointeurs sont-ils utilisés dans des applications à faible mémoire critique?

Je n'ai pas fait beaucoup de travail là-bas, mais je suppose que oui. L'accès au pointeur est un aspect élémentaire de la programmation d'assemblage. Il faut des quantités insignifiantes de mémoire et les opérations de pointeur sont rapides - même dans le contexte de ce type d'applications.

35
Telastyn

Je n'ai pas tout à fait le même tour que Telastyn.

Les globaux du système dans un processeur intégré peuvent être adressés avec des adresses spécifiques codées en dur.

Les globaux dans un programme seront traités comme un décalage à partir d'un pointeur spécial qui pointe vers l'endroit en mémoire où les globaux et les statiques sont stockés.

Les variables locales apparaissent lorsqu'une fonction est entrée et sont traitées comme un décalage par rapport à un autre pointeur spécial, souvent appelé "pointeur de trame". Cela inclut les arguments de la fonction. Si vous faites attention aux push et pops avec le pointeur de pile, vous pouvez supprimer le pointeur de trame et accéder aux variables locales directement à partir du pointeur de pile.

Vous payez donc pour l'indirection des pointeurs, que vous parcouriez un tableau ou que vous saisissiez simplement une variable locale ou globale banale. Il est simplement basé sur un pointeur différent, en fonction de sa variable. Le code bien compilé gardera ce pointeur dans un registre CPU, plutôt que de le recharger chaque fois qu'il est utilisé.

11

Oui bien sûr. Mais c'est un équilibre.

Les applications à faible mémoire sont généralement construites en tenant compte du compromis entre la surcharge de quelques variables de pointeur par rapport à la surcharge de ce qui serait un programme massif (qui doit être stocké en mémoire, rappelez-vous! ) si les pointeurs ne pouvaient pas être utilisés.

Cette considération s'applique aux programmes tous, car personne ne veut créer un horrible gâchis impossible à maintenir avec du code dupliqué à gauche et au centre, c'est vingt fois plus grand qu'il ne devrait l'être.

6

Dans des langages comme C et C++, tout en utilisant des pointeurs vers des variables, nous avons besoin d'un emplacement mémoire supplémentaire pour stocker cette adresse. N'est-ce pas une surcharge de mémoire?

Vous supposez que le pointeur doit être stocké. Ce n'est pas toujours le cas. Chaque variable est stockée à une adresse mémoire. Supposons que vous avez un long déclaré comme long n = 5L;. Cela alloue du stockage pour n à une certaine adresse. Nous pouvons utiliser cette adresse pour faire des choses fantaisistes comme *((char *) &n) = (char) 0xFF; pour manipuler des parties de n. L'adresse de n n'est stockée nulle part comme surcharge supplémentaire.

Comment est-ce compensé?

Même si les pointeurs sont explicitement stockés (par exemple dans des structures de données telles que des listes), la structure de données résultante est souvent plus élégante (plus simple, plus facile à comprendre, plus facile à manipuler, etc.) qu'une structure de données équivalente sans pointeurs.

Les pointeurs sont-ils utilisés dans des applications à faible mémoire critique?

Oui. Les appareils qui utilisent des microcontrôleurs contiennent souvent très peu de mémoire, mais le micrologiciel peut utiliser des pointeurs pour gérer les vecteurs d'interruption ou la gestion des tampons, etc.

5
Lawrence

Avoir un pointeur consomme certainement des frais généraux, mais vous pouvez également voir l'avantage: le pointeur est comme l'index. En C, vous pouvez utiliser des structures de données complexes comme des chaînes et des structures en raison de pointeurs uniquement.

Supposons en fait que vous vouliez passer une variable par référence, puis il est facile de maintenir un pointeur plutôt que de répliquer toute la structure et de synchroniser les changements entre elles (même pour les copier, vous aurez besoin d'un pointeur). Comment feriez-vous face aux allocations et désallocations de mémoire non contiguës sans pointeur?

Même vos variables normales ont une entrée dans la table des symboles qui stocke l'adresse vers laquelle votre variable pointe. Donc, je ne pense pas que cela crée beaucoup de surcharge en termes de mémoire (seulement 4 ou 8 octets). Même les langages comme Java utilisent des pointeurs en interne (référence), ils ne vous permettent tout simplement pas de les manipuler car cela rendrait la JVM moins sécurisée.

Vous ne devez utiliser des pointeurs que lorsque vous n'avez pas d'autre choix que des types de données manquants, les structures (en c) car l'utilisation de pointeurs peut entraîner des erreurs si elles ne sont pas gérées correctement et sont relativement plus difficiles à déboguer.

5
Krrish Raj

N'est-ce donc 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'emplacement des éléments de la 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 Poussez 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. Le programme doit non seulement garder une trace du morceau de données audio en mémoire, mais aussi où il est 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 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 effectivement en dur l'adresse 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 les applications à faible mémoire critique?

Oui, très souvent, car de nombreuses applications critiques pour les performances sont écrites en C ou C++, dominées par l'utilisation du pointeur (elles peuvent se trouver 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 qui est utilisé pour garder la trace de 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 "moche" 8 Mo est toujours la taille du cache L3. Ici, j'ai utilisé "maussade" plus 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:

  1. 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.

  2. 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 jeu 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 performances 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 de manière séquentielle 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é):

enter image description here

Lorsque nous commençons à rechercher des pointeurs et à accéder au nœud Foo, nous commençons par un échec obligatoire (et éventuellement une erreur 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 donc:

enter image description here

Cela nous amène à mettre en cache (éventuellement aussi à paginer) une région de mémoire uniquement pour accéder à une partie de celle-ci et à expulser le reste lorsque 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:

enter image description here

... 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.

3
user204677

N'est-ce pas une surcharge de mémoire?

Il s'agit en effet d'une surcharge de mémoire, mais très petite (au point d'être insignifiante).

Comment est-ce compensé?

Il n'est pas compensé. Vous devez comprendre que l'accès aux données via un pointeur (déréférencer un pointeur) est extrêmement rapide (si je me souviens bien, il utilise une seule instruction d'assemblage par déréférence). Il est suffisamment rapide pour être dans bien des cas l'alternative la plus rapide que vous ayez.

Les pointeurs sont-ils utilisés dans des applications à faible mémoire critique?

Oui.

1
utnapistim

Vous n'avez besoin que de l'utilisation supplémentaire de la mémoire (4 à 8 octets par pointeur, généralement) pendant que vous en avez besoin. Il existe de nombreuses techniques qui rendent cela plus abordable.

La technique la plus fondamentale qui rend les pointeurs puissants est que vous n'avez pas besoin de conserver chaque pointeur. Parfois, vous pouvez utiliser un algorithme pour construire un pointeur à partir d'un pointeur vers autre chose. L'exemple le plus trivial de ceci est l'arithmétique des tableaux. Si vous allouez un tableau de 50 entiers, vous n'avez pas besoin de conserver 50 pointeurs, un pour chaque entier. Vous gardez généralement un pointeur (le premier) et utilisez l'arithmétique du pointeur pour générer les autres à la volée. Parfois, vous pouvez conserver temporairement l'un de ces pointeurs vers un élément spécifique du tableau, mais uniquement pendant que vous en avez besoin. Une fois que vous avez terminé, vous pouvez le supprimer, tant que vous avez conservé suffisamment d'informations pour le régénérer plus tard, si vous en avez besoin. Cela peut sembler trivial, mais c'est exactement le type d'outils de conservation que vous utiliserez si vous vous souciez vraiment du nombre de pointeurs que vous conservez en mémoire.

Dans les situations de mémoire extrêmement serrée, cela peut être utilisé pour minimiser les coûts. Si vous travaillez dans un très espace mémoire restreint, vous avez généralement une bonne idée du nombre d'objets que vous devez manipuler. Au lieu d'allouer un groupe d'entiers un à la fois et de garder des pointeurs complets vers eux, vous pouvez profiter de votre connaissance de développeur que vous n'aurez jamais plus de 256 entiers dans cet algorithme particulier. Dans ce cas, vous pouvez conserver un pointeur sur le premier entier et suivre un index à l'aide d'un caractère (1 octet) plutôt que d'utiliser un pointeur complet (4/8 octets). Vous pouvez également utiliser des astuces algorithmiques pour générer certains de ces indices à la volée.

Ce genre de conscience de mémoire était très populaire dans le passé. Par exemple, les jeux NES s'appuieraient largement sur leur capacité à créer des données et à générer des pointeurs de manière algorithmique plutôt que de les stocker tous en gros.

Les situations de mémoire extrêmes peuvent également conduire à faire des choses comme l'allocation de tous les espaces sur lesquels vous opérez au moment de la compilation. Ensuite, le pointeur que vous devez stocker dans cette mémoire est stocké dans le programme plutôt que dans les données. Dans de nombreuses situations de mémoire limitée, vous avez un programme et une mémoire de données séparés (souvent ROM vs RAM), vous pouvez donc être en mesure d'ajuster la façon dont vous utilisez votre algorithme pour pousser les pointeurs dans cette mémoire de programme .

Fondamentalement, vous ne pouvez pas vous débarrasser de tous les frais généraux. Cependant, vous pouvez le contrôler. En utilisant des techniques algorithmiques, vous pouvez minimiser le nombre de pointeurs que vous pouvez stocker. Si vous utilisez des pointeurs vers la mémoire dynamique, vous n'obtiendrez jamais en dessous du coût de la conservation d'un pointeur vers cet emplacement de mémoire dynamique, car c'est la quantité minimale d'informations nécessaires pour accéder à quoi que ce soit dans ce bloc de mémoire. Cependant, dans les scénarios de contraintes de mémoire ultra-resserrées, cela a tendance à être le cas spécial (les contraintes de mémoire dynamique et de mémoire ultra-resserrées ont tendance à ne pas apparaître dans les mêmes situations).

0
Cort Ammon

Dans de nombreuses situations, les pointeurs économisent de la mémoire. Une alternative courante à l'utilisation de pointeurs est de faire une copie d'une structure de données. Une copie complète d'une structure de données sera plus grande qu'un pointeur.

Un exemple d'application à temps critique est une pile réseau. Une bonne pile réseau sera conçue pour être "zéro copie" - et pour ce faire, il faut utiliser intelligemment des pointeurs.

0
paj28