Allocation dynamique de mémoire. Allocation de mémoire en C (fonction malloc) Mot-clé réservé pour l'allocation de mémoire dynamique

Un programme peut stocker des informations dans la mémoire principale de l'ordinateur de deux manières principales. Le premier utilise des variables globales et locales, notamment des tableaux, des structures et des classes. Dans le cas de variables locales globales et statiques, l'emplacement de stockage des informations est fixe pour toute la durée d'exécution du programme. Dans le cas de variables locales, la mémoire est allouée sur la pile. Bien que Borland C++ gère ces variables de manière très efficace, leur utilisation nécessite que le programmeur connaisse à l'avance la quantité de mémoire qui sera requise lors de l'exécution du programme.

La deuxième façon de stocker des informations consiste à utiliser le système d’allocation dynamique de mémoire Borland C++. Dans cette méthode, la mémoire pour stocker les informations est allouée à partir d'une zone de mémoire libre selon les besoins et restituée, c'est-à-dire libéré lorsque le besoin en a disparu. La zone de mémoire libre se situe entre la zone mémoire où se trouve le programme et la pile. Cette zone est appelée tas et est utilisée pour les demandes d'allocation de mémoire dynamique.

L’avantage de l’utilisation de la mémoire dynamique est que la même mémoire peut être utilisée pour stocker différentes informations lors de l’exécution du programme. Puisque la mémoire est allouée dans un but précis et libérée une fois son utilisation terminée, la même mémoire peut être utilisée à un autre moment à d’autres fins dans une autre partie du programme. Un autre avantage de l'allocation dynamique de mémoire est qu'elle peut être utilisée pour créer des listes chaînées, des arbres binaires et d'autres structures de données dynamiques.

Le cœur de l'allocation dynamique de mémoire en C réside dans les fonctions malloc() et free(), qui font partie de la bibliothèque standard. Chaque fois qu'une demande d'allocation de mémoire est effectuée par la fonction malloc(), une partie de la mémoire libre disponible est allouée. Chaque fois que cette mémoire est libérée à l'aide de la fonction free(), cette mémoire est renvoyée au système.

Le langage C++ définit deux opérateurs pour l'allocation dynamique de mémoire : new et delete.

La norme ANSI C définit uniquement quatre fonctions d'allocation dynamique de mémoire : calloc(), malloc(), free() et realloc(). Cependant, Borland C++ contient plusieurs autres fonctions d'allocation dynamique de mémoire. Lors de la compilation du code pour le modèle de mémoire moderne 32 bits, la mémoire est plate et généralement seules les quatre fonctions d'allocation de mémoire standard sont utilisées.

La norme ANSI C précise que les informations d'en-tête requises pour l'allocation dynamique de mémoire sont contenues dans le fichier stdlib.h. Cependant, Borland C++ autorise l'utilisation de fichiers d'en-tête stdlib.h ou alloc.h. Ici, nous utilisons le fichier d'en-tête stdlib.h car il offre la portabilité. Certaines autres fonctions d'allocation dynamique de mémoire nécessitent des fichiers d'en-tête alloc.h, malloc.h ou dos.h. Vous devez accorder une attention particulière au fichier d'en-tête nécessaire pour utiliser chaque fonction.

Donc. Le troisième type, le plus intéressant pour nous dans ce sujet, est le type de mémoire dynamique.

Comment travaillions-nous avec les tableaux auparavant ? int a Comment travaillons-nous maintenant ? Nous allouons autant que nécessaire :

#inclure < stdio.h> #inclure < stdlib.h> int main() (size_t taille; // Crée un pointeur vers int // – essentiellement un tableau vide. int *liste; scanf("%lu" , &taille); // Alloue de la mémoire pour les éléments de taille de taille int// et notre "tableau vide" fait désormais référence à cette mémoire.< size; ++i) { scanf (list = (int *)malloc(size * sizeof(int)); < size; ++i) { printf (list = (int *)malloc(size * sizeof(int)); pour (int je = 0 ; je " %d ", *(liste + i)); // *

)

// N'oubliez pas de nettoyer après vous !

gratuit(liste); )

Vide * malloc(size_t size);

#inclure < stdio.h> #inclure < stdlib.h> Mais en général, il s'agit d'une fonction qui alloue la taille des octets de mémoire non initialisée (pas des zéros, mais des déchets). scanf( Si l'allocation a réussi, un pointeur vers le tout premier octet de mémoire allouée est renvoyé.< size; ++i) { scanf (list = (int *)malloc(size * sizeof(int)); En cas d'échec - NULL. De plus, errno sera égal à ENOMEM (nous examinerons cette merveilleuse variable plus tard). Autrement dit, il serait plus correct d'écrire :< size; ++i) { printf (list = (int *)malloc(size * sizeof(int)); int main () ( size_t taille; int *list; scanf ( // *

, &taille); list = (int *)malloc(size * sizeof(int));

#inclure < stdlib.h> if (list == NULL ) ( goto error; ) for (int i = 0 ; i

, liste + je);

) pour (int je = 0 ; je

    , *(liste + i));

    Tout comme malloc allouera de la mémoire pour compter les objets de taille en octets. La mémoire allouée est initialisée avec des zéros.

    void * realloc (void *ptr, size_t size);

    Réaffecte (si possible) la mémoire pointée par ptr par taille d'octets. S'il n'y a pas assez d'espace pour augmenter la mémoire allouée pointée par ptr , realloc crée une nouvelle allocation (allocation), copie les anciennes données pointées par ptr , libère l'ancienne allocation et renvoie un pointeur vers la mémoire allouée.

    Si ptr est NULL, la réallocation est identique à l'appel de malloc.

    Si size est zéro et que ptr n'est pas NULL , un morceau de mémoire de la taille minimale est alloué et celui d'origine est libéré.

    void * reallocf (void *ptr, size_t size);

    Une idée de l'API FreeBSD. Comme realloc , mais si la réallocation échoue, il efface le pointeur accepté.

    void * valloc(size_t taille);

    Comme malloc, mais la mémoire allouée est alignée sur les pages.

En C++, comme dans de nombreux autres langages, la mémoire peut être allouée de manière statique (la mémoire est allouée avant le début de l'exécution du programme et libérée une fois le programme terminé) ou dynamiquement (la mémoire est allouée et libérée pendant l'exécution du programme).

L'allocation de mémoire statique est effectuée pour toutes les variables globales et locales qui ont des déclarations explicites dans le programme (sans utilisation de pointeurs). Dans ce cas, le mécanisme d'allocation de mémoire est déterminé par l'emplacement de la description de la variable dans le programme et le spécificateur de classe mémoire dans la description. Le type d'une variable détermine la taille de la zone mémoire allouée, mais le mécanisme d'allocation de mémoire ne dépend pas du type. Il existe deux mécanismes principaux pour l'allocation de mémoire statique.

· La mémoire pour chacune des variables globales et statiques (déclarées avec le spécificateur statique) ​​est allouée avant le début de l'exécution du programme conformément à la description du type. Du début à la fin de l'exécution du programme, ces variables sont associées à la zone mémoire qui leur est allouée. Ils ont donc une durée de vie globale, mais leur portée est différente.

· Pour les variables locales déclarées à l'intérieur d'un bloc et sans spécificateur statique la mémoire est allouée d'une manière différente. Avant que le programme ne commence à s'exécuter (lorsqu'il est chargé), une zone mémoire assez grande est allouée, appelée empiler(parfois les termes sont utilisés pile de programmes ou pile d'appels pour faire une distinction entre la pile en tant que type de données abstrait). La taille de la pile dépend de l'environnement de développement, par exemple, dans MS Visual C++, par défaut 1 mégaoctet est alloué à la pile (cette valeur peut être personnalisée). Lors de l'exécution du programme, à l'entrée d'un certain bloc, de la mémoire est allouée sur la pile pour les variables localisées dans le bloc (conformément à la description de leur type) ; à la sortie du bloc, cette mémoire est libérée. Ces processus sont exécutés automatiquement, c'est pourquoi les variables locales en C++ sont souvent appelées automatique.

Lorsqu'une fonction est appelée, la mémoire est allouée sur la pile pour ses variables locales, ses paramètres (la valeur ou l'adresse du paramètre est placée sur la pile), le résultat de la fonction et l'enregistrement d'un point de retour - l'adresse dans le programme où vous devez revenir une fois la fonction terminée. Lorsqu'une fonction se termine, toutes les données qui lui sont associées sont supprimées de la pile.

L'utilisation du terme « pile » est facile à expliquer : avec l'approche acceptée en matière d'allocation et de libération de mémoire, les variables placées en dernier sur la pile (ce sont les variables localisées dans le bloc imbriqué le plus profond) en sont d'abord supprimées. Autrement dit, l'allocation et la libération de la mémoire s'effectuent selon le principe LIFO (LAST IN – FIRST OUT, last in – first out). C'est le principe de fonctionnement d'une pile. Nous examinerons la pile en tant que structure de données dynamique et sa possible implémentation dans la section suivante.



Dans de nombreux cas, la mémoire allouée statiquement conduit à une utilisation inefficace (cela est particulièrement vrai pour les grands tableaux), car la zone mémoire allouée statiquement n'est pas toujours réellement remplie de données. Par conséquent, en C++, comme dans de nombreux langages, il existe des moyens pratiques de générer dynamiquement des variables. L'essence de l'allocation dynamique de mémoire est que la mémoire est allouée (capturée) à la demande du programme et également libérée sur demande. Dans ce cas, la taille de la mémoire peut être déterminée par le type de variable ou explicitement spécifiée dans la requête. De telles variables sont appelées dynamique. La possibilité de créer et d'utiliser des variables dynamiques est étroitement liée au mécanisme de pointeur.

En résumant tout ce qui précède, nous pouvons imaginer le schéma d'allocation de mémoire suivant lors de l'exécution du programme (Figure 2.1). L'emplacement des zones les unes par rapport aux autres sur la figure est tout à fait arbitraire, car Le système d'exploitation s'occupe des détails de l'allocation de mémoire.

Figure 2.1 – diagramme de répartition de la mémoire

Pour conclure cette section, abordons un problème douloureux lorsque l'on travaille avec la pile : la possibilité de son débordement (cette situation d'urgence est généralement appelée Débordement de pile). La raison à l'origine du problème est claire : la quantité limitée de mémoire allouée à la pile lors du chargement d'un programme. Les situations les plus probables de débordement de pile sont les grands tableaux locaux et l'imbrication profonde des appels de fonctions récursives (ce qui se produit généralement lorsque la programmation des fonctions récursives est inexacte, par exemple, une branche de terminal est oubliée).



Afin de mieux comprendre le problème de débordement de pile, nous vous recommandons de réaliser cette expérience simple. En fonction principal déclarez un tableau d'entiers d'une taille d'un million d'éléments, par exemple. Le programme se compilera, mais lorsque vous l'exécuterez, une erreur de débordement de pile se produira. Ajoutez maintenant le spécificateur au début de la description du tableau statique(ou retirez la déclaration de tableau de la fonction principal) – le programme fonctionnera !

Il n'y a rien de miraculeux à cela - c'est juste que maintenant le tableau n'est pas placé sur la pile, mais dans la zone des variables globales et statiques. La taille de la mémoire pour cette zone est déterminée par le compilateur - si le programme est compilé, il s'exécutera.

Cependant, en règle générale, il n'est pas nécessaire de déclarer des tableaux de grande taille générés statiquement dans un programme. Dans la plupart des cas, l'allocation dynamique de mémoire pour ces données sera une méthode plus efficace et plus flexible.

C++ prend en charge trois types principaux décharge(ou bien "distribution") mémoire, dont deux que nous connaissons déjà :

Allocation de mémoire statique vaut pour et les variables. La mémoire est allouée une seule fois, au démarrage du programme, et est conservée tout au long du programme.

Allocation automatique de mémoire est valable pour et . La mémoire est allouée lors de l'entrée dans le bloc contenant ces variables, et supprimée lors de sa sortie.

Allocation de mémoire dynamique est le sujet de cette leçon.

Allocation variable dynamique

L'allocation de mémoire statique et automatique a deux propriétés communes :

Comment fonctionne l’allocation dynamique de mémoire ?

Votre ordinateur dispose de mémoire (peut-être la majeure partie) qui peut être utilisée par les programmes. Lorsque vous exécutez un programme, votre système d'exploitation charge ce programme dans une partie de cette mémoire. Et cette mémoire utilisée par votre programme est divisée en plusieurs parties, chacune effectuant une tâche spécifique. Une partie contient votre code, l'autre est utilisée pour effectuer des opérations normales (suivi des fonctions appelées, création et destruction de variables globales et locales, etc.). Nous en reparlerons plus tard. Cependant, la majeure partie de la mémoire disponible reste simplement là, en attente des demandes d'allocation des programmes.

Lorsque vous allouez dynamiquement de la mémoire, vous demandez au système d'exploitation de réserver une partie de cette mémoire pour une utilisation par votre programme. Si le système d'exploitation peut répondre à cette demande, l'adresse de cette mémoire est renvoyée à votre programme. Désormais, votre programme pourra utiliser cette mémoire quand il le souhaite. Lorsque vous avez déjà effectué tout ce qui était nécessaire avec cette mémoire, elle doit être renvoyée au système d'exploitation pour être répartie entre d'autres requêtes.

Contrairement à l'allocation de mémoire statique ou automatique, le programme lui-même est responsable de la demande et du renvoi de la mémoire allouée dynamiquement.

Libérer de la mémoire

Lorsque vous allouez dynamiquement une variable, vous pouvez également l'initialiser via une initialisation uniforme (en C++11) :

int *ptr1 = nouvel int (7); // utilise l'initialisation directe int *ptr2 = new int ( 8 ); // utilise l'initialisation uniforme

Lorsque tout ce qui était nécessaire a déjà été fait avec une variable allouée dynamiquement, vous devez indiquer explicitement à C++ de libérer cette mémoire. Pour les variables, cela se fait en utilisant opérateur supprimer:

// Supposons que ptr ait été précédemment alloué à l'aide de l'opérateur new delete ptr ; // renvoie la mémoire pointée par ptr au système d'exploitation ptr = 0; // fait de ptr un pointeur nul (utilisez nullptr au lieu de 0 en C++11)

L’opérateur delete ne supprime rien. Il renvoie simplement la mémoire précédemment allouée au système d'exploitation. Le système d'exploitation peut alors réaffecter cette mémoire à une autre application (ou à nouveau à la même application).

Même s'il peut sembler que nous supprimons variable, mais ce n'est pas vrai ! Une variable pointeur a toujours la même portée qu’auparavant et peut se voir attribuer une nouvelle valeur comme n’importe quelle autre variable.

Notez que la suppression d'un pointeur qui ne pointe pas vers la mémoire allouée dynamiquement peut entraîner des problèmes.

Panneaux suspendus

C++ ne donne aucune garantie sur ce qui arrivera au contenu de la mémoire libérée ou à la valeur du pointeur supprimé. Dans la plupart des cas, la mémoire renvoyée au système d'exploitation contiendra les mêmes valeurs qu'avant. libération, et le pointeur restera pointant uniquement vers la mémoire déjà libérée (supprimée).

Un pointeur pointant vers la mémoire libérée est appelé signe suspendu. Le déréférencement ou la suppression d’un pointeur suspendu produira des résultats inattendus. Considérons le programme suivant :

#inclure int main() ( int *ptr = new int; *ptr = 8; // place la valeur dans l'emplacement mémoire alloué delete ptr; // renvoie la mémoire au système d'exploitation. ptr est maintenant un pointeur pendant std :: cout<< *ptr; // разыменование висячего указателя приведёт к неожиданным результатам delete ptr; // попытка освободить память снова приведёт к неожиданным результатам также return 0; }

#inclure

int principal()

int * ptr = nouveau int ; // alloue dynamiquement une variable entière

* ptr = 8 ; // place la valeur dans la cellule mémoire allouée

supprimer ptr ; // renvoie la mémoire au système d'exploitation. ptr est maintenant un pointeur suspendu

std :: cout<< * ptr ; // le déréférencement d'un pointeur suspendu entraînera des résultats inattendus

supprimer ptr ; // essayer à nouveau de libérer de la mémoire entraînera également des résultats inattendus

renvoie 0 ;

Dans le programme ci-dessus, la valeur 8, qui était précédemment affectée à une variable dynamique, peut ou non continuer à être là après sa libération. Il est également possible que la mémoire libérée ait déjà été allouée à une autre application (ou pour l'usage propre du système d'exploitation), et tenter d'y accéder entraînera la fermeture automatique de votre programme par le système d'exploitation.

Le processus de libération de mémoire peut également conduire à la création plusieurs panneaux suspendus. Prenons l'exemple suivant :

#inclure int main() ( int *ptr = new int; // alloue dynamiquement une variable entière int *otherPtr = ptr; // otherPtr pointe maintenant vers la même mémoire allouée que ptr delete ptr; // renvoie la mémoire au système d'exploitation. ptr et otherPtr sont maintenant des pointeurs suspendus ptr = 0; // ptr est maintenant nullptr // Cependant, otherPtr est toujours un pointeur suspendu return 0;

#inclure

int principal()

int * ptr = nouveau int ; // alloue dynamiquement une variable entière

int * autrePtr = ptr ; // otherPtr pointe désormais vers la même mémoire allouée que ptr

supprimer ptr ; // renvoie la mémoire au système d'exploitation. ptr et otherPtr sont maintenant des pointeurs suspendus

ptr = 0 ; // ptr est maintenant nullptr

// Cependant, otherPtr est toujours un pointeur suspendu !

renvoie 0 ;

Tout d’abord, essayez d’éviter les situations dans lesquelles plusieurs pointeurs pointent vers la même partie de la mémoire allouée. Si cela n'est pas possible, indiquez clairement quel pointeur "possède" la mémoire (et est responsable de sa suppression) et quels pointeurs y accèdent simplement.

Deuxièmement, lorsque vous supprimez un pointeur et s'il ne se ferme pas immédiatement après la suppression, il doit alors être rendu nul, c'est-à-dire attribuez la valeur 0 (ou en C++11). Par « sortir de la portée immédiatement après la suppression », nous entendons que vous supprimez le pointeur à la toute fin du bloc dans lequel il est déclaré.

Règle : définissez les pointeurs supprimés sur 0 (ou nullptr en C++11) à moins qu'ils ne soient hors de portée immédiatement après avoir été supprimés.

Opérateur nouveau

Lors d'une demande de mémoire auprès du système d'exploitation, dans de rares cas, elle peut ne pas être disponible (c'est-à-dire qu'elle peut ne pas être disponible).

Par défaut, si l'opérateur new n'a pas fonctionné, la mémoire n'a pas été allouée, alors exception mauvais_alloc. Si cette exception n'est pas gérée correctement (et elle le sera, puisque nous n'avons pas encore examiné les exceptions et leur gestion), alors le programme cessera simplement de s'exécuter (plantage) avec une erreur d'exception non gérée.

Dans de nombreux cas, le processus de levée d'une exception avec l'opérateur new (ainsi que le blocage du programme) n'est pas souhaitable, il existe donc une forme alternative de l'opérateur new qui renvoie un pointeur nul si la mémoire ne peut pas être allouée. Il vous suffit d'ajouter constante std :: nothrow entre le nouveau mot-clé et le type de données :

int *value = new (std::nothrow) int; // le pointeur de valeur deviendra nul si l'allocation dynamique d'une variable entière échoue

Dans l'exemple ci-dessus, si new ne renvoie pas de pointeur avec de la mémoire allouée dynamiquement, alors un pointeur nul sera renvoyé.

Le déréférencer n'est pas non plus recommandé, car cela entraînerait des résultats inattendus (très probablement un crash du programme). Par conséquent, la meilleure pratique consiste à vérifier toutes les demandes d’allocation de mémoire pour s’assurer qu’elles se terminent correctement et que la mémoire est allouée :

int *value = new (std::nothrow) int; // demande d'allouer de la mémoire dynamique pour une valeur entière if (!value) // gère le cas où new renvoie null (c'est-à-dire que la mémoire n'est pas allouée) ( // gère ce cas std::cout<< "Could not allocate memory"; }

La non-allocation de mémoire par l'opérateur new étant extrêmement rare, les programmeurs oublient généralement d'effectuer cette vérification !

Pointeurs nuls et allocation de mémoire dynamique

Les pointeurs nuls (pointeurs avec la valeur 0 ou nullptr) sont particulièrement utiles lors du processus d'allocation dynamique de mémoire. Leur présence nous informe comme si : « Aucune mémoire n’est allouée à ce pointeur ». Et ceci, à son tour, peut être utilisé pour effectuer une allocation de mémoire conditionnelle :

// Si ptr n'a pas encore reçu de mémoire, allouez-la if (!ptr) ptr = new int;

La suppression du pointeur nul n'a aucun effet. Ce qui suit n'est donc pas nécessaire :

if (ptr) supprimer ptr ;

si (ptr)

supprimer ptr ;

Au lieu de cela, vous pouvez simplement écrire :

supprimer ptr ;

Si ptr n'est pas nul, alors la variable allouée dynamiquement sera supprimée. Si la valeur du pointeur est nulle, rien ne se passera.

Fuite de mémoire

La mémoire allouée dynamiquement n'a aucune portée, c'est-à-dire il reste alloué jusqu'à ce qu'il soit explicitement libéré ou jusqu'à ce que votre programme termine son exécution (et que le système d'exploitation efface lui-même tous les tampons mémoire). Cependant, les pointeurs utilisés pour stocker les adresses mémoire allouées dynamiquement suivent les règles de portée des variables régulières. Cet écart peut provoquer un comportement intéressant. Par exemple:

void doSomething() ( int *ptr = new int; )

Travailler avec de la mémoire dynamique constitue souvent un goulot d'étranglement dans de nombreux algorithmes, à moins que des astuces spéciales ne soient utilisées.

Dans cet article, j'examinerai quelques-unes de ces techniques. Les exemples de l'article diffèrent (par exemple, de) en ce sens que la surcharge des opérateurs new et delete est utilisée, et de ce fait, les structures syntaxiques seront minimalistes et la refonte du programme sera simple. Les pièges rencontrés dans le processus sont également décrits (bien entendu, les gourous qui lisent la norme d’un bout à l’autre ne seront pas surpris).

0. Avons-nous besoin d’un travail manuel avec la mémoire ?

Tout d'abord, vérifions dans quelle mesure un allocateur intelligent peut accélérer le travail de la mémoire.

Écrivons des tests simples pour C++ et C# (C# est connu pour son excellent gestionnaire de mémoire, qui divise les objets en générations, utilise différents pools pour des objets de différentes tailles, etc.).

Noeud de classe ( public : Node* next ; ); // ... pour (int i = 0; i< 10000000; i++) { Node* v = new Node(); }

Noeud de classe ( public Node next; ) // ... pour (int l = 0; l< 10000000; l++) { var v = new Node(); }

Malgré toute la nature « vide sphérique » de l’exemple, la différence de temps était de 10 fois (62 ms contre 650 ms). De plus, l'exemple C# est terminé, et selon les règles de savoir-vivre en C++, les objets sélectionnés doivent être supprimés, ce qui va encore augmenter l'écart (jusqu'à 2580 ms).

1. Pool d'objets

La solution évidente consiste à prendre un gros bloc de mémoire du système d'exploitation et à le diviser en blocs égaux de taille sizeof(Node), lors de l'allocation de mémoire, à retirer le bloc du pool et, lors de sa libération, à le remettre dans le pool. Le moyen le plus simple d’organiser un pool consiste à utiliser une liste à chaînage unique (pile).

Puisque l’objectif est une intervention minimale dans le programme, tout ce qui peut être fait est d’ajouter le mixin BlockAlloc à la classe Node :
Nœud de classe : public BlockAlloc

Tout d'abord, nous avons besoin d'un pool de gros blocs (pages), que nous extrayons du système d'exploitation ou du runtime C. Il peut être organisé au-dessus des fonctions malloc et free, mais pour une plus grande efficacité (pour éviter le niveau d'abstraction supplémentaire), nous utilisons VirtualAlloc/VirtualFree. Ces fonctions allouent de la mémoire par multiples de blocs de 4 Ko et réservent également de l'espace d'adressage de processus par multiples de blocs de 64 Ko. En spécifiant simultanément les options de validation et de réserve, nous franchissons un autre niveau d'abstraction, en réservant l'espace d'adressage et en allouant des pages de mémoire en un seul appel.

Classe PagePool

inline size_t align(size_t x, size_t a) ( return ((x-1) | (a-1)) + 1; ) //#define align(x, a) ((((x)-1) | ( (a)-1)) + 1) modèle class PagePool ( public: void* GetPage() ( void* page = VirtualAlloc(NULL, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page); return page; ) ~PagePool() ( pour (vecteur ::itérateur i = pages.begin(); je != pages.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) privé : vecteur pages ; );

Ensuite on organise un pool de blocs d'une taille donnée

Classe BlockPool

modèle classe BlockPool : PagePool ( public: BlockPool() : head(NULL) ( BlockSize = align(sizeof(T), Alignment); count = PageSize / BlockSize; ) void* AllocBlock() ( // todo: lock(this) if (!head) FormatNewPage(); void* tmp = head; head = *(void**)head; void FreeBlock(void* tmp) ( // à faire : lock(this) *(void**)tmp = head; head = tmp; ) privé : void* head; size_t count; void FormatNewPage(); head = tmp;< count-1; i++) { void* next = (char*)tmp + BlockSize; *(void**)tmp = next; tmp = next; } *(void**)tmp = NULL; } };

Commentaire // tâche : verrouiller (ceci) Emplacements marqués qui nécessitent une synchronisation inter-thread (par exemple, utilisez EnterCriticalSection ou boost::mutex).

Je vais vous expliquer pourquoi lors du « formatage » d'une page, l'abstraction FreeBlock n'est pas utilisée pour ajouter un bloc au pool. Si quelque chose comme ça était écrit

Pour (size_t je = 0; je< PageSize; i += BlockSize) FreeBlock((char*)tmp+i);

Ensuite, la page, selon le principe FIFO, serait balisée « à l'envers » :

Plusieurs blocs demandés consécutivement au pool auraient des adresses décroissantes. Mais le processeur n'aime pas revenir en arrière, cela casse Prefetch ( MISE À JOUR: Non pertinent pour les processeurs modernes). Si vous faites du balisage en boucle
pour (size_t i = PageSize-(BlockSize-(PageSize%BlockSize)); i != 0; i -= BlockSize) FreeBlock...
alors le cycle de balisage reviendrait vers les adresses.

Maintenant que les préparatifs sont terminés, nous pouvons décrire le cours de mixin.
modèle class BlockAlloc ( public: static void* opérateur new(size_t s) ( if (s != sizeof(T)) ( return::operator new(s); ) return pool.AllocBlock(); ) static void opérateur delete(void * m, size_t s) ( if (s != sizeof(T)) ( ::operator delete(m); ) else if (m != NULL) ( pool.... static void* opérateur new(size_t, void * m) ( return m; ) // ...et l'avertissement concernant le placement manquant delete... static void Operator delete(void*, void*) ( ) private: static BlockPool piscine; ); modèle Pool de blocs BloquerAllocer ::piscine;

Je vais vous expliquer pourquoi des contrôles sont nécessaires si (s != taillede(T))
Quand travaillent-ils ? Ensuite, lorsqu'une classe héritée de la base T est créée/supprimée.
Les héritiers utiliseront le new/delete habituel, mais BlockAlloc peut également être mélangé avec eux. De cette façon, nous pouvons déterminer facilement et en toute sécurité quelles classes doivent utiliser les pools sans craindre de casser quelque chose dans le programme. L'héritage multiple fonctionne également très bien avec ce mixin.

Prêt. Nous héritons de Node de BlockAlloc et réexécutons le test.
Le temps de test est désormais de 120 ms. 5 fois plus rapide. Mais en C#, l'allocateur est encore meilleur. Ce n'est probablement pas seulement une liste chaînée. (Si immédiatement après new nous appelons immédiatement delete, et ainsi ne gaspillons pas beaucoup de mémoire, en plaçant les données dans le cache, nous obtenons 62 ms. Étrange. Exactement comme le .NET CLR, comme s'il renvoyait immédiatement les variables locales libérées au pool correspondant, sans attendre GC)

2. Conteneur et son contenu coloré

Rencontrez-vous souvent des classes qui stockent de nombreux objets enfants différents, de telle sorte que la durée de vie de ces derniers n'est pas plus longue que la durée de vie du parent ?

Par exemple, il peut s'agir d'une classe XmlDocument remplie de classes Node et Attribute, ainsi que de chaînes C (char*) extraites du texte à l'intérieur des nœuds. Ou une liste de fichiers et de répertoires dans le gestionnaire de fichiers qui sont chargés une fois lorsque le répertoire est relu et ne changent plus jamais.

Comme nous l'avons montré dans l'introduction, la suppression coûte plus cher que la création de nouveaux. L'idée de la deuxième partie de l'article est d'allouer de la mémoire aux objets enfants dans un gros bloc associé à l'objet Parent. Lorsqu'un objet parent est supprimé, les destructeurs enfants seront appelés, comme d'habitude, mais la mémoire n'aura pas besoin d'être restituée - elle sera libérée en un seul gros bloc.

Créons une classe PointerBumpAllocator qui peut extraire des morceaux de différentes tailles d'un gros bloc et allouer un nouveau gros bloc lorsque l'ancien est épuisé.

Classe PointerBumpAllocator

modèle class PointerBumpAllocator ( public: PointerBumpAllocator() : free(0) ( ) void* AllocBlock(size_t block) ( // todo: lock(this) block = align(block, Alignment); if (block > free) ( free = align (bloc, PageSize); head = GetPage(free); void* tmp = head = (char*)head + return tmp; ::itérateur i = pages.begin(); je != pages.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) private: void* GetPage(size_t size) ( void* page = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page) ; page de retour ; pages ;<>tête vide* ;

size_t gratuit ; ); typedef PointerBumpAllocator

Allocateur par défaut ; Enfin, décrivons un mixin ChildObject avec un new et un delete surchargés accédant à un allocateur donné :

Modèle

struct ChildObject ( static void* opérateur new(size_t s, A& allocator) ( return allocator.AllocBlock(s); ) static void* Operator new(size_t s, A* allocator) ( return allocator->AllocBlock(s); ) static opérateur void delete(void*, size_t) ( ) // *1 opérateur void statique delete(void*, A*) ( ) opérateur void statique delete(void*, A&) ( ) privé : opérateur void* statique new(size_t s );

Dans ce cas, en plus d'ajouter un mixin à la classe enfant, vous devrez également corriger tous les appels à new (ou utiliser le modèle « factory »). La syntaxe du nouvel opérateur sera la suivante :
Nouveau (...paramètres pour l'opérateur...) ChildObject (...paramètres pour le constructeur...)
Pour plus de commodité, j'ai spécifié deux nouveaux opérateurs qui acceptent A& ou A*.
Si l'allocateur est ajouté à la classe parent en tant que membre, la première option est plus pratique :
node = new(allocator) XmlNode(nodename);

Il n'y a pas de syntaxe spéciale pour appeler delete ; le compilateur appellera la suppression standard (marquée *1), quel que soit le nouvel opérateur utilisé pour créer l'objet. Autrement dit, la syntaxe de suppression est normale :
supprimer le nœud ;

Si une exception se produit dans le constructeur de ChildObject (ou son successeur), delete est appelé avec une signature correspondant à la signature du nouvel opérateur utilisé pour créer cet objet (le premier paramètre size_t sera remplacé par void*).

Placer le nouvel opérateur dans la section privée protège contre l’appel de new sans spécifier d’allocateur.

Voici un exemple complet d'utilisation de la paire Allocator-ChildObject :

Exemple

class XmlDocument : public DefaultAllocator ( public : ~XmlDocument() ( pour (vecteur ::itérateur i = nodes.begin(); je != nodes.end(); ++i) ( delete (*i); ​​​​​) ) void AddNode(char* content, char* nom) ( char* c = (char*)AllocBlock(strlen(content)+1); strcpy(c, content ); char* n = (char*)AllocBlock(strlen(name)+1); nodes.push_back(new(this) XmlNode(c, n) ) classe XmlNode : public ChildObject ( public: XmlNode(char* _content, char* _name) : content(_content), name(_name) ( ) private: char* content; char* nom; ); privé : vecteur nœuds ; );

Conclusion. L'article a été écrit il y a un an et demi pour le bac à sable, mais hélas, le modérateur ne l'a pas aimé.



Des questions ?

Signaler une faute de frappe

Texte qui sera envoyé à nos rédacteurs :