Problèmes de mémoire
Le modèle de mémoire
Le compilateur Free Pascal émet du code 32 bits ou 64 bits. Cela a plusieurs conséquences :
- Vous avez besoin d'un microprocesseur 32 bits ou 64 bits pour exécuter le code généré.
- Vous n'avez pas besoin de vous soucier des sélecteurs de segments. La mémoire peut être adressée à l'aide d'un seul pointeur 32 bits (sur les microprocesseurs 32 bits) ou 64 bits (sur les microprocesseurs 64 bits avec adressage 64 bits). La quantité de mémoire est limitée uniquement par la quantité de mémoire (virtuelle) disponible sur votre machine.
- Les structures que vous définissez sont de taille illimitée. Les tableaux peuvent être aussi longs que vous le souhaitez. Vous pouvez demander des blocs de mémoire de n'importe quelle taille.
Formats de données
Cette section donne des informations sur l'espace d'entreposage occupé par les différents types possibles en Free Pascal. Des informations sur l'alignement interne seront également fournies.
Types entiers
Dans le cas de types définis par l'utilisateur, l'espace d'entreposage occupé dépend des limites du type :
- Si les deux limites sont comprises entre -128 et 127, la variable est entreposée sous forme ShortInt (quantité signée de 8 bits).
- Si les deux limites sont comprises entre 0 et 255, la variable est entreposée sous forme Byte (quantité de 8 bits non signée).
- Si les deux limites sont comprises entre -32768 et 32767, la variable est entreposée sous forme de SmallInt (quantité signée de 16 bits).
- Si les deux limites sont comprises entre 0 et 65535, la variable est entreposée sous forme Word (quantité non signée de 16 bits).
- Si les deux limites sont comprises entre 0 et 4294967295, la variable est entreposée sous forme LongWord (quantité 32 bits non signée).
- Sinon la variable est entreposée sous forme LongInt (quantité signée de 32 bits).
Types Char
Un caractère, ou une sous-intervalle du type Char, est entreposé sous forme d'octet. Un WideChar est entreposé sous forme de mot, soit 2 octets.
Types Boolean
Le type Boolean est entreposé sous forme d'octet et peut prendre la valeur true ou false.
Un ByteBool est entreposé sous forme d'octet, un type WordBool est entreposé sous forme de mot et un LongBool est entreposé sous forme d'entier long (LongInt).
Types d'énumération
Par défaut, toutes les énumérations sont entreposées sous forme de mot long (4 octets), ce qui équivaut à spécifier les commutateurs {$Z4}, {$PACKENUM 4} ou {$PACKENUM DEFAULT}.
Ce comportement par défaut peut être modifié par les commutateurs du compilateur et par le mode du compilateur.
En mode compilateur tp, ou pendant que les commutateurs {$Z1} ou {$PACKENUM 1} sont en vigueur, l'espace d'entreposage utilisé est indiqué dans le tableau suivant :
Nombre d'éléments dans l'énumération | Espace d'entreposage utilisé |
---|---|
0..255 | Byte (1 octet) |
256..65535 | Word (2 octets) |
> 65535 | LongWord (4 octets) |
Lorsque les commutateurs {$Z2} ou {$PACKENUM 2} sont actifs, la valeur est entreposée sur 2 octets (un mot), si l'énumération comporte moins ou égale à 65535 éléments. S'il y a plus d'éléments, la valeur d'énumération est entreposée sous forme de valeur de 4 octets (un mot long).
Types à virgule flottante
La taille et la cartographie des types à virgule flottante varient d'un microprocesseur à l'autre. À l'exception de l'architecture Intel 80x86, le type étendu correspond au type double IEEE si un coprocesseur matériel à virgule flottante est présent.
Les types à virgule flottante ont un format binaire d'entreposage divisé en trois champs distincts : la mantisse, l'exposant et le bit de signe entrepose le signe de la valeur à virgule flottante.
Single
Le type Single occupe 4 octets d'espace d'entreposage et sa structure de mémoire est la même que celle du type unique IEEE-754. Ce type est le seul dont la disponibilité est garantie sur toutes les plates-formes dotées d'une émulation matérielle ou logicielle. Cela signifie que la plateforme AVR par exemple ne dispose pas de ce type.
Le format de mémoire du format Single ressemble à ce qui est montré dans la figure suivante :
Double
Le type Double occupe 8 octets d'espace d'entreposage et sa structure de mémoire est la même que celle du type double IEEE-754.
Le format mémoire du double format ressemble à ce qui est montré sur la figure suivante :
Sur les microprocesseurs ne supportant pas les opérations coprocesseurs (et qui disposent du commutateur {$E+}), le type Double n'existe pas.
Extended
Pour les microprocesseurs Intel 80x86, le type étendu occupe 10 octets d'espace mémoire.
Pour tous les autres microprocesseurs prenant en charge les opérations en virgule flottante, le type étendu est un surnom pour le type prenant en charge le plus de précision, il s'agit généralement du type Double. Sur les processeurs qui ne prennent pas en charge les opérations du coprocesseur (et disposant du commutateur {$E+}), le type étendu correspond généralement au type unique.
Comp
Pour les microprocesseurs Intel 80x86, le type Comp contient une valeur intégrale de 63 bits et un bit de signe (en position MSB). Le type Comp utilise 8 octets d'espace d'entreposage.
Sur les autres processeurs, le type Comp n'est pas pris en charge.
Real
Contrairement à Turbo Pascal, où le type Real avait un format interne spécial, sous Free Pascal, le type Real correspond simplement à l'un des autres types réels. Il correspond au type Double sur les microprocesseurs prenant en charge les opérations à virgule flottante, tandis qu'il correspond au type Single sur les microprocesseurs ne prenant pas en charge les opérations à virgule flottante dans le matériel. Voir le tableau suivant pour plus d'informations à ce sujet :
Microprocesseur | Cartographie de type Real |
---|---|
Intel 80x86 | Double |
Motorola 680x0 (avec commutateur {$E-}) | Double |
Motorola 680x0 (avec commutateur {$E+}) | Single |
Types Pointer
Un type de Pointer est entreposé sous forme de LongWord (valeur non signée de 32 bits) sur les microprocesseurs 32 bits, et est entreposé sous forme de valeur non signée de 64 bits sur les microprocesseurs 64 bits.
Types String
Types AnsiString
Le type AnsiString est une chaîne de caractères allouée dynamiquement n'ayant aucune limitation de longueur (autre que la mémoire adressable). Lorsque la chaîne de caractères n'est plus référencée (son compteur de références atteint zéro), sa mémoire est automatiquement libérée.
Si la chaîne de caractères AnsiString est une constante, alors son compteur de références sera égal à -1, indiquant qu'elle ne doit jamais être libérée. La structure en mémoire d'une AnsiString est présentée dans le tableau suivant pour les programmes 32 bits, et le deuxième tableau pour les programmes 64 bits :
Structure de mémoire AnsiString (modèle 32 bits) :
Déplacement | Contient |
---|---|
-12 | Indicateur de page de codes (2 octets). |
-10 | Taille des caractères (2 octets). |
-8 | LongInt avec décompte de références. |
-4 | LongInt avec la taille réelle de la chaîne de caractères. |
0 | Tableau actuel de caractères, terminé par un caractère nul. |
Structure de mémoire AnsiString (modèle 64 bits) :
Déplacement | Contient |
---|---|
-24 | Indicateur de page de codes (2 octets). |
-22 | Taille des caractères (2 octets) |
-16 | Sizeint avec nombre de références. |
-8 | Sizeint avec la taille actuelle de la chaîne de caractères. |
0 | Tableau actuelle de caractères, terminé par un caractère nul. |
Types ShortString
Une ShortString occupe autant d'octets que sa longueur maximale plus un. Le premier octet contient la longueur dynamique actuelle de la chaîne de caractères. Les octets suivants contiennent les caractères réels (de type char) de la chaîne de caractères. La taille maximale d'une chaîne de caractères courte est l'octet de longueur suivi de 255 caractères.
Types Unicodestring
Une chaîne de caractères Unicode est allouée sur la mémoire de tas, un peu comme une chaîne de caractères AnsiString. Contrairement à la chaîne de caractères AnsiString, une chaîne de caractères Unicode prend 2 octets par caractère et se termine par un double null. C'est une référence comptée tout comme un AnsiString. La structure en mémoire d'une chaîne de caractères UniCode est présentée dans le tableau suivant pour les programmes 32 bits et dans le deuxième tableau pour les programmes 64 bits :
Structure de la mémoire UnicodeString (modèle 32 bits) :
Déplacement | Contient |
---|---|
-8 | LongInt avec décompte de références. |
-4 | LongInt avec la taille réelle de la chaîne. |
0 | Tableau actuel de caractères, terminé par un caractère nul. |
Structure de la mémoire UnicodeString (modèle 64 bits) :
Déplacement | Contient |
---|---|
-16 | SizeInt avec nombre de références. |
-8 | SizeInt avec la taille actuelle de la chaîne de caractères. |
0 | Tableau réel de caractères, terminé par un caractère nul. |
Types WideString
Sur les plateformes non Windows, le type chaîne de caractères WideString est égal au type chaîne de caractères UniCode.
Sur les plates-formes Windows, une chaîne de caractères WideString est allouée sur la mémoire de tas, un peu comme une chaîne de caractères Unicode, et se termine par un double null. Contrairement à une chaîne de caractères Unicode, les références ne sont pas comptées.
Types Set
Un ensemble est entreposé sous forme de tableau de bits, où chaque bit indique si l'élément est dans l'ensemble ou exclu de l'ensemble. Le nombre maximum d'éléments dans un ensemble est de 256.
Le nombre d'octets utilisés par le compilateur pour entreposer un ensemble dépend du mode. Pour les modes non Delphi, le compilateur entrepose les petits ensembles (moins de 32 éléments) dans un LongInt, si l'intervalle de types d'éléments Set le permet. Cela permet un traitement plus rapide et réduit la taille du programme. Sinon, les ensembles sont entreposés sur 32 octets.
Pour le mode Delphi, le compilateur essaie d'entreposer l'ensemble dans le plus petit nombre d'octets dans lequel l'ensemble peut tenir.
Le programme suivant illustre ceci :
- Program ExempleSet;
-
- Type
- TSet1=Set of 0..1;
- TSet2=Set of 0..8;
- TSet3=Set of 0..16;
- TSet4=Set of 0..128;
- TSet5=Set of 0..247;
- TSet6=Set of 0..255;
- BEGIN
- WriteLn('Set 1: ',SizeOf(TSet1));
- WriteLn('Set 2: ',SizeOf(TSet2));
- WriteLn('Set 3: ',SizeOf(TSet3));
- WriteLn('Set 4: ',SizeOf(TSet4));
- WriteLn('Set 5: ',SizeOf(TSet5));
- WriteLn('Set 6: ',SizeOf(TSet6));
- END.
Lorsqu'il est compilé en mode fpc, le programme génère ce qui suit :
Set 1: 4Set 2: 4
Set 3: 4
Set 4: 32
Set 5: 32
Set 6: 32
En mode Delphi, le programme affichera :
Set 1: 1Set 2: 2
Set 3: 4
Set 4: 17
Set 5: 31
Set 6: 32
Notez que la taille de l'élément défini peut être influencée avec la directive {$PACKSET }. Le nombre LongWord d'un élément E spécifique est donné par :
et le numéro de bit dans cette valeur de 32 bits est donné par :
Types de tableaux statiques
Un tableau statique est entreposé sous la forme d'une séquence contiguë de variables des composantes du tableau. Les composantes avec les index les plus bas sont entreposés en premier en mémoire. Aucun alignement n'est effectué entre chaque élément du tableau. Un tableau multidimensionnel est entreposé avec la dimension la plus à droite augmentant en premier (ordre des lignes principales).
Types de tableaux dynamiques
Un tableau dynamique est entreposé sous forme de pointeur vers un bloc de mémoire sur la mémoire de tas. La mémoire sur le tas est une séquence contiguë de variables des composantes du tableau, tout comme pour un tableau statique. Le nombre de références et la taille de la mémoire sont entreposés en mémoire juste avant le démarrage réel du tableau, avec un déplacement négatif par rapport à l'adresse à laquelle le pointeur fait référence. Il ne devrait pas être utilisé.
Types Record
Chaque champ d'un Record est entreposé dans une séquence contiguë de variables, où le premier champ est entreposé à l'adresse la plus basse de la mémoire. En cas de champs variantes dans un enregistrement, chaque variante commence à la même adresse en mémoire. Les champs d'un enregistrement sont généralement alignés, sauf si la directive packed est spécifiée lors de la déclaration du type Record.
Types Object
Les objets (Object) sont entreposés en mémoire comme des enregistrements ordinaires avec un champ supplémentaire : un pointeur vers la table de méthodes virtuelles (VMT). Ce champ est entreposé en premier, et tous les champs de l'objet sont entreposés dans l'ordre dans lequel ils sont déclarés (avec alignement possible des adresses de champs, sauf si l'objet a été déclaré comme étant compressé).
Le VMT est initialisé par l'appel à la méthode Constructor de l'objet. Si l'opérateur new a été utilisé pour appeler le constructeur, les champs de données de l'objet seront entreposés dans la mémoire de tas, sinon ils seront directement entreposés dans la section données de l'exécutable final.
Si un objet n'a pas de méthodes virtuelles, aucun pointeur vers un VMT n'est inséré.
La mémoire allouée ressemble au tableau suivant :
Déplacement 32 bits | Déplacement 64 bits | Contenu |
---|---|---|
+0 | +0 | Pointeur vers VMT (facultatif). |
+4 | +8 | Données. Tous les champs dans l'ordre dans lequel ils ont été déclarés. |
... | ... | ... |
La table de méthodes virtuelles (VMT) pour chaque type d'objet se compose de 2 champs de contrôle (contenant la taille des données), d'un pointeur vers la VMT de l'ancêtre de l'objet (Nil s'il n'y a pas d'ancêtre), puis de pointeurs vers toutes les méthodes virtuelles. La disposition VMT est illustrée dans le tableau suivant (Le VMT est construit par le compilateur) :
Déplacement 32 bits | Déplacement 64 bits | Contenu |
---|---|---|
+0 | +0 | Taille des données de type d'objet. |
+4 | +8 | Moins la taille des données de type d'objet. Permet de déterminer les pointeurs VMT valides. |
+8 | +16 | Pointeur vers l'ancêtre VMT, Nil si aucun ancêtre disponible. |
+12 | +24 | Pointeurs vers les méthodes virtuelles. |
... | ... | ... |
Types de classes
Tout comme les objets, les classes sont entreposées en mémoire comme des enregistrements ordinaires avec un champ supplémentaire : un pointeur vers la table de méthodes virtuelles (VMT). Ce champ est entreposé en premier et tous les champs de la classe sont entreposés dans l'ordre dans lequel ils sont déclarés.
Contrairement aux objets, tous les champs de données d'une classe sont toujours entreposés dans la mémoire de tas.
La mémoire allouée ressemble au tableau suivant :
Déplacement 32 bits | Déplacement 64 bits | Contenu |
---|---|---|
+0 | +0 | Pointeur vers VMT. |
+4 | +8 | Données. Tous les champs dans l'ordre dans lequel ils ont été déclarés. |
... | ... | ... |
La table de méthodes virtuelles (VMT) de chaque classe se compose de plusieurs champs utilisés pour les informations sur le type d'exécution. La disposition VMT est illustrée dans le tableau suivant (Le VMT est construit par le compilateur) :
Déplacement 32 bits | Déplacement 64 bits | Contenu |
---|---|---|
+0 | +0 | Taille des données de type d'objet. |
+4 | +8 | Moins la taille des données de type d'objet. Permet de déterminer les pointeurs VMT valides. |
+8 | +16 | Pointeur vers l'ancêtre VMT, Nil si aucun ancêtre disponible. |
+12 | +24 | Pointeur vers le nom de la classe (entreposé sous forme de chaîne de caractères courte). |
+16 | +32 | Pointeur vers la table de méthodes dynamiques (utilisant un message avec des entiers). |
+20 | +40 | Pointeur vers la table de définition de méthode. |
+24 | +48 | Pointeur vers la table de définition de champ. |
+28 | +56 | Pointeur vers le type de table d'informations. |
+32 | +64 | Pointeur vers la table d'initialisation de l'instance. |
+36 | +72 | Pointeur vers la table automatique. |
+40 | +80 | Pointeur vers la table d'interface. |
+44 | +88 | Pointeur vers la table de méthodes dynamiques (utilisant un message avec des chaînes de caractères). |
+48 | +96 | Pointeur vers le destructeur Destroy. |
+52 | +104 | Pointeur vers la méthode NewInstance. |
+56 | +112 | Pointeur vers la méthode FreeInstance. |
+60 | +120 | Pointeur vers la méthode SafeCallException. |
+64 | +128 | Pointeur vers la méthode DefaultHandler. |
+68 | +136 | Pointeur vers la méthode AfterConstruction. |
+72 | +144 | Pointeur vers la méthode BeforeDestruction. |
+76 | +152 | Pointeur vers la méthode DefaultHandlerStr. |
+80 | +160 | Pointeur vers la méthode Dispatch. |
+84 | +168 | Pointeur vers la méthode DispatchStr. |
+88 | +176 | Pointeur vers la méthode Equals. |
+92 | +184 | Pointeur vers la méthode GetHashCode. |
+96 | +192 | Pointeur vers la méthode ToString. |
+100 | +200 | Pointeurs vers d'autres méthodes virtuelles. |
... | ... | ... |
Voir le fichier rtl/inc/objpash.inc pour les informations VMT les plus récentes.
Types de fichier
Les types de fichiers sont représentés sous forme d'enregistrements. Les fichiers typés et les fichiers non typés sont représentés sous forme d'enregistrement fixe :
Les fichiers texte sont décrits à l'aide de l'enregistrement suivant :
- TextBuf=Array[0..255] of Char;
- Textrec=Packed Record
- Handle:THandle;
- Mode:LongInt;
- BufSize:SizeInt;
- _private:SizeInt;
- BufPos:SizeInt;
- BufEnd:SizeInt;
- BufPtr:^TextBuf;
- OpenFunc:Pointer;
- InoutFunc:Pointer;
- FlushFunc:Pointer;
- CloseFunc:Pointer;
- UserData:Array[1..32] of Byte;
- Name:Array[0..255] of Char;
- LineEnd:TLineEndStr;
- Buffer:TextBuf;
- End;
Champ | Description |
---|---|
Handle | Le champ Handle renvoie le descripteur du fichier (si le fichier est ouvert), tel que renvoyé par le système d'exploitation. |
Mode | Le champ mode peut prendre plusieurs valeurs. Lorsqu'il est fmclosed, le fichier est fermé et le champ Handle n'est pas valide. Lorsque la valeur est égale à fminput, cela indique que le fichier est ouvert en lecture seulement. fmoutput indique un accès en écriture seule et fminout indique un accès en lecture-écriture au fichier. |
Name | Le champ de nom est une chaîne de caractères terminée par un caractère nul représentant le nom du fichier. |
UserData | Le champ UserData n'est jamais utilisé par les routines de gestion de fichiers Free Pascal et peut être utilisé à des fins spéciales par les développeurs de logiciels. |
Types procéduraux
Un type procédural est entreposé sous forme de pointeur générique, entreposant l'adresse de la routine.
Un type procédural vers une procédure ou une fonction normale est entreposé sous forme de pointeur générique, entreposant l'adresse du point d'entrée de la routine.
Dans le cas d'une méthode de type procédural, l'entreposage est constitué de deux pointeurs, le premier étant un pointeur vers le point d'entrée de la méthode, et le second étant un pointeur vers self (l'instance de l'objet).
Alignement des données
Constantes typées et alignement des variables
Toutes les données statiques (variables et constantes typées) supérieures à un octet sont généralement alignées sur une limite multiple de deux. Cet alignement s'applique uniquement à l'adresse de début des variables, et non à l'alignement des champs au sein de structures ou d'objets par exemple. L'alignement est similaire sur les différents processeurs cibles.
Taille des données (octets) | Alignement (petite taille) | Alignement (rapide) |
---|---|---|
1 | 1 | 1 |
2 à 3 | 2 | 2 |
4 à 7 | 2 | 4 |
8+ | 2 | 4 |
Les colonnes d'alignement indiquent l'alignement de l'adresse de la variable, c'est-à-dire que l'adresse de début de la variable sera alignée sur cette limite. L'alignement de petite taille est valide lorsque le code généré doit être optimisé pour la taille (option du compilateur -Os) et non pour la vitesse, sinon l'alignement rapide est utilisé pour aligner les données (c'est la valeur par défaut).
Alignement des types structurés
Par défaut, tous les éléments d'une structure sont alignés selon l'alignement naturel. A moins que la directive $PACKRECORDS ou le modificateur Packed ne soit utilisé pour aligner les données d'une autre manière.
La mémoire de tas
La mémoire de tas est utilisé pour entreposer toutes les variables dynamiques et pour entreposer les instances de classe. L'interface avec la mémoire de tas est la même que dans Turbo Pascal et Delphi même si les effets ne sont peut-être pas les mêmes. La mémoire de tas est thread-safe, donc l'allocation de mémoire à partir de différents processus léger ne pose pas de problème.
Stratégie d'allocation de la mémoire de tas
La mémoire de tas est une structure mémoire organisée sous forme de pile. Le fond de la mémoire de tas est entreposé dans la variable HeapOrg. Initialement, le pointeur de la mémoire de tas (HeapPtr) pointe vers le bas de la mémoire de tas. Lorsqu'une variable est allouée sur la mémoire de tas, HeapPtr est incrémenté de la taille du bloc mémoire alloué. Cela a pour effet d'empiler les variables dynamiques les unes sur les autres.
Chaque fois qu'un bloc est alloué, sa taille est normalisée pour avoir une granularité de 16 (ou 32 sur les systèmes 64 bits) octets.
Lorsque Dispose ou FreeMem est appelé pour supprimer un bloc de mémoire ne se trouvant pas en haut de la mémoire de tas, la mémoire de tas devient fragmenté. Les routines de désallocation ajoutent également les blocs libérés à la liste libre étant en fait une liste chaînée de blocs libres. De plus, si la taille du bloc désalloué était inférieure à 8 Ko, le cache de liste libre est également mis à jour.
Le cache de liste libre est en fait un cache de blocs de tas libres ayant des longueurs spécifiques (la taille de bloc ajustée divisée par 16 donne l'index dans la table du cache de liste libre). Il est plus rapide d'accéder que de rechercher dans l'ensemble de la liste libre.
Le format d'une entrée dans la FreeList est le suivant :
Le champ Next pointe vers le prochain bloc libre, tandis que le champ Prev pointe vers le bloc libre précédent. L'algorithme d'allocation de mémoire est le suivant :
- La taille du bloc à allouer est ajustée à une granularité de 16 (ou 32) octets.
- La liste libre mise en cache est recherchée pour trouver un bloc libre de la taille spécifiée ou supérieure, si c'est le cas, il est alloué et la routine se termine.
- La liste libre est parcourue pour trouver un bloc libre de la taille spécifiée ou de taille supérieure, si c'est le cas, il est alloué et la routine se termine.
- S'il n'est pas trouvé dans la liste libre, la mémoire de tas est agrandi pour allouer la mémoire spécifiée et la routine se termine.
- Si la mémoire de tas ne peut plus être développé en interne, la bibliothèque d'exécution génère une erreur d'exécution 203.
La mémoire de tas s'agrandit
La mémoire de tas alloue la mémoire du système d'exploitation en fonction des besoins.
La mémoire du système d'exploitation est demandée par blocs : il tente d'abord d'augmenter la mémoire par bloc de 64 Ko si la taille à allouer est inférieure à 64 Ko, ou à 256 Ko ou 1 024 Ko dans le cas contraire. Si cela échoue, il essaie d'augmenter la mémoire de tas du montant que vous avez demandé à la mémoire de tas.
Si la tentative de réservation de mémoire du système d'exploitation échoue, la valeur renvoyée dépend de la valeur de la variable globale ReturnNilIfGrowHeapFails. Ceci est résumé dans le tableau suivant :
Valeur ReturnNilIfGrowHeapFails | Action du gestionnaire de mémoire par défaut |
---|---|
FALSE | (Par défaut) Erreur d'exécution 203 générée |
TRUE | GetMem, ReallocMem et New renvoient zéro. |
ReturnNilIfGrowHeapFails peut être défini pour modifier le comportement du gestionnaire d'erreurs du gestionnaire de mémoire par défaut.
Débogage de la mémoire de tas
Free Pascal fournit une unité vous permettant de tracer l'allocation et la désallocation de la mémoire tas : heaptrc.
Si vous spécifiez le commutateur -gh sur la ligne de commande, le gestionnaire de mémoire tracera ce qui est alloué et désalloué, et à la sortie de votre programme, un résumé sera envoyé à la sortie standard.
Écrire votre propre gestionnaire de mémoire
Free Pascal vous permet d'écrire et d'utiliser votre propre gestionnaire de mémoire. Les fonctions standard GetMem, FreeMem, ReallocMem,... utilisent un enregistrement spécial dans l'unité System pour effectuer la gestion réelle de la mémoire. L'unité centrale initialise cet enregistrement avec son propre gestionnaire de mémoire, mais vous pouvez lire et définir cet enregistrement à l'aide des appels GetMemoryManager et SetMemoryManager :
L'enregistrement TMemoryManager est défini comme suit :
- TMemoryManager=Record
- NeedLock:Boolean;
- Getmem:Function(Size:PtrInt):Pointer;
- Freemem:Function(Var P:Pointer):PtrInt;
- FreememSize:Function(Var P:Pointer;Size:PtrInt):PtrInt;
- AllocMem:Function(Size:PtrInt):Pointer;
- ReAllocMem:Function(Var p:pointer;Size:PtrInt):Pointer;
- MemSize:Function(P:Pointer):PtrInt;
- InitThread:Procedure;
- DoneThread:Procedure;
- RelocateHeap:Procedure;
- GetHeapStatus:Function:THeapStatus;
- GetFPCHeapStatus:Function:TFPCHeapStatus;
- End;
Comme vous pouvez le constater, les éléments de cet enregistrement sont pour la plupart des variables procédurales. L'unité System ne fait rien d'autre que d'appeler ces différentes variables lorsque vous allouez ou libérez de la mémoire. Chacun de ces champs correspond à l'appel correspondant dans l'unité System. Nous décrirons chacun d'eux :
Champ | Description |
---|---|
NeedLock | Ce booléen indique si le gestionnaire de mémoire a besoin d'un verrou : si le gestionnaire de mémoire lui-même n'est pas thread-safe, alors cela peut être défini sur True et les routines de mémoire utiliseront un verrou pour toutes les routines de mémoire. Si ce champ est défini sur False, aucun verrou ne sera utilisé. |
Getmem | Cette fonction alloue un nouveau bloc sur le tas. Le bloc doit être long en octets. La valeur de retour est un pointeur vers le bloc nouvellement alloué. |
Freemem | Doit libérer un bloc précédemment alloué. Le pointeur P pointe vers un bloc précédemment alloué. Le gestionnaire de mémoire doit implémenter un mécanisme pour déterminer la taille du bloc de mémoire. La valeur de retour est facultative et peut être utilisée pour renvoyer la taille de la mémoire libérée. |
FreememSize | Cette fonction doit libérer la mémoire pointée par P. Le paramètre Size est la taille attendue du bloc mémoire pointé par P. Ceci doit être ignoré, mais peut être utilisé pour vérifier le comportement du programme. |
AllocMem | Est identique à GetMem, seule la mémoire allouée doit être remplie de zéros avant le retour de l'appel. |
ReAllocMem | Doit allouer un bloc de mémoire de la taille spécifiée et le remplir avec le contenu du bloc de mémoire pointé par P, en le tronquant à la nouvelle taille nécessaire. Après cela, la mémoire pointée par P peut être libérée. La valeur de retour est un pointeur vers le nouveau bloc mémoire. Notez que P peut être Nil, auquel cas le comportement est équivalent à GetMem. |
MemSize | Doit renvoyer la taille du bloc mémoire P. Cette fonction peut renvoyer zéro si le gestionnaire de mémoire ne permet pas de déterminer cette information. |
InitThread | Cette routine est appelée lorsqu'un nouveau processus léger est démarré : elle doit initialiser les structures de tas pour le processus léger actuel (le cas échéant). |
DoneThread | Cette routine est appelée lorsqu'un processus léger est terminé : elle doit nettoyer toutes les structures de mémoire de tas pour le processus léger actuel. |
RelocateHeap | Déplace la mémoire de tas - ceci concerne uniquement les tas locaux de processus léger. |
GetHeapStatus | Doit renvoyer un enregistrement THeapStatus avec l'état du gestionnaire de mémoire. Cet enregistrement doit être rempli de valeurs conformes à Delphi. |
GetHeapStatus | Doit renvoyer un enregistrement TFPCHeapStatus avec l'état du gestionnaire de mémoire. Cet enregistrement doit être rempli avec des valeurs conformes à FPC. |
Pour implémenter votre propre gestionnaire de mémoire, il suffit de construire un tel enregistrement et d'appeler SetMemoryManager.
Pour éviter les conflits avec le gestionnaire de mémoire système, la configuration du gestionnaire de mémoire doit avoir lieu le plus tôt possible lors de l'initialisation de votre programme, c'est-à-dire avant le traitement de tout appel à GetMem.
Cela signifie en pratique que l'unité implémentant le gestionnaire de mémoire doit être la première dans la clause Uses de votre programme ou bibliothèque, puisqu'elle sera alors initialisée avant toutes les autres unités - à l'exception de l'unité System elle-même, bien sûr.
Cela signifie également qu'il n'est pas possible d'utiliser l'unité heaptrc en combinaison avec un gestionnaire de mémoire personnalisé, puisque l'unité heaptrc utilise le gestionnaire de mémoire système pour effectuer toutes ses allocations. Placer l'unité heaptrc après l'unité implémentant le gestionnaire de mémoire écraserait l'enregistrement du gestionnaire de mémoire installé par le gestionnaire de mémoire personnalisé, et vice versa.
L'unité suivante montre une implémentation simple d'un gestionnaire de mémoire personnalisé utilisant le gestionnaire de mémoire de la bibliothèque C. Il est distribué sous forme de paquet avec Free Pascal :
- Unit cmem;
-
- INTERFACE
-
- Const
- LibName = 'libc';
-
- Function Malloc(Size:PtrInt):Pointer; cdecl; External LibName Name 'malloc';
- Procedure Free(P:Pointer);cdecl; External LibName Name 'free';
- Function ReAlloc(P:Pointer;Size:PtrInt):Pointer;cdecl; External LibName Name 'realloc';
- Function CAlloc(UnitSize,UnitCount:PtrInt):Pointer;cdecl; External LibName Name 'calloc';
-
- IMPLEMENTATION
-
- Type
- PPtrInt=^PtrInt;
-
- Function CGetMem(Size:PtrInt):Pointer;Begin
- CGetMem:=Malloc(Size+sizeof(ptrint));
- If(CGetMem <> NIL)Then Begin
- PPtrInt(CGetMem)^:=Size;
- Inc(CGetMem,sizeof(PtrInt));
- End;
- End;
-
- Function CFreeMem(P:Pointer):PtrInt;Begin
- If(P <> NIL)Then Dec(P,Sizeof(PtrInt));
- Free(P);
- CFreeMem:=0;
- End;
-
- Function CFreeMemSize(P:Pointer;Size:PtrInt):PtrInt;Begin
- If Size<=0 Then Begin
- If size<0 Then RunError(204);
- Exit;
- End;
- If(P <> NIL)Then Begin
- If(Size<>PPtrInt(P-SizeOf(PtrInt))^)Then RunError(204);
- End;
- CFreeMemSize:=CFreeMem(P);
- End;
-
- Function CAllocMem(Size:PtrInt):Pointer;Begin
- CAllocMem:=CAlloc(Size+SizeOf(PtrInt),1);
- If(CAllocMem <> NIL)Then Begin
- PPtrInt(CAllocMem)^:=Size;
- Inc(CAllocMem,SizeOf(PtrInt));
- End;
- End;
-
- Function CReAllocMem(Var P:Pointer;Size:PtrInt):Pointer;Begin
- If size=0 Then Begin
- If P<>NIL Then Begin
- Dec(P,SizeOf(PtrInt));
- Free(P);
- P:=NIL;
- End;
- End
- Else
- Begin
- Inc(Size,SizeOf(PtrInt));
- If P=NIL Then P:=Malloc(Size)
- Else
- Begin
- Dec(P,SizeOf(PtrInt));
- P:=ReAlloc(P,Size);
- End;
- If(P <> NIL)Then Begin
- PPtrInt(p)^:=Size-SizeOf(PtrInt);
- Inc(P,SizeOf(PtrInt));
- End;
- End;
- CReAllocMem:=P;
- End;
-
- Function CMemSize(P:Pointer):PtrInt;Begin
- CMemSize:=PPtrInt(P-SizeOf(PtrInt))^;
- End;
-
- Function CGetHeapStatus:THeapStatus;
- Var
- res:THeapStatus;
- Begin
- Fillchar(Res,SizeOf(Res),0);
- CGetHeapStatus:=res;
- End;
-
- Function CGetFPCHeapStatus:TFPCHeapStatus;Begin
- FillChar(CGetFPCHeapStatus,SizeOf(CGetFPCHeapStatus),0);
- End;
-
- Const
- CMemoryManager:TMemoryManager=(
- NeedLock:False;
- GetMem:@CGetmem;
- FreeMem:@CFreeMem;
- FreememSize:@CFreememSize;
- AllocMem:@CAllocMem;
- ReallocMem:@CReAllocMem;
- MemSize:@CMemSize;
- InitThread:NIL;
- DoneThread:NIL;
- RelocateHeap:NIL;
- GetHeapStatus:@CGetHeapStatus;
- GetFPCHeapStatus:@CGetFPCHeapStatus;
- );
-
- Var
- OldMemoryManager:TMemoryManager;
-
- INITIALIZATION
- GetMemoryManager(OldMemoryManager);
- SetMemoryManager(CmemoryManager);
- FINALIZATION
- SetMemoryManager(OldMemoryManager);
- END.
Utilisation de la mémoire DOS sous le Go32 extender
Étant donné que Free Pascal pour DOS est un compilateur 32 bits et utilise un DOS extender, l'accès à la mémoire DOS n'est pas trivial. Ce qui suit est une tentative d'explication sur la façon d'accéder et d'utiliser le DOS ou la mémoire en mode réel.
En mode protégé, la mémoire est accessible via les sélecteurs et les déplacements. Vous pouvez considérer les sélecteurs comme les équivalents en mode protégé des segments.
En Free Pascal, un pointeur est un déplacement dans le sélecteur DS, pointant vers les données de votre programme. Pour accéder à la mémoire DOS (en mode réel), vous avez besoin d'un sélecteur pointant vers la mémoire DOS. L'unité go32 vous fournit un tel sélecteur : la variable DosMemSelector, comme on l'appelle commodément.
Vous pouvez également allouer de la mémoire dans l'espace mémoire du DOS, en utilisant la fonction Global_DOS_Alloc de l'unité go32. Cette fonction allouera de la mémoire à un endroit où le DOS la verra. À titre d'exemple, voici une fonction renvoyant de la mémoire en mode réel du DOS et renvoie une paire sélecteur:déplacement pour celle-ci :
(Vous devez libérer cette mémoire à l'aide de la fonction Global_DOS_Free.)
Vous pouvez accéder à n'importe quel emplacement de la mémoire à l'aide d'un sélecteur. Vous pouvez obtenir un sélecteur en utilisant la fonction :
puis laissez ce sélecteur pointer vers la mémoire physique souhaitée en utilisant la fonction :
Sa longueur peut être réglée à l'aide de la fonction :
Vous pouvez manipuler la mémoire pointée par le sélecteur en utilisant les fonctions de l'unité GO32. Par exemple avec la fonction seg_fillchar. Après avoir utilisé le sélecteur, vous devez le libérer à nouveau grâce à la fonction :
Lors du portage du code Turbo Pascal
Le fait que le code 16 bits ne soit plus utilisé signifie que certaines des anciennes constructions et fonctions de Turbo Pascal sont obsolètes. Voici une liste de fonctions ne devant plus être utilisées :
Fonction | Description |
---|---|
Seg() | Renvoie le segment d'une adresse mémoire. Étant donné que les segments n'ont plus de signification, zéro est renvoyé dans l'implémentation de la bibliothèque d'exécution Free Pascal de Seg. |
Ofs() | Renvoie le déplacement d'une adresse mémoire. Puisque les segments n'ont plus de signification, l'adresse complète est renvoyée dans l'implémentation Free Pascal de cette fonction. Cela a pour conséquence que le type de retour est LongInt ou Int64 au lieu de Word. |
Cseg(), Dseg() | Renvoyé, respectivement, les segments de code et de données de votre programme. Cela renvoie zéro dans l'implémentation Free Pascal de l'unité System, puisque le code et les données sont dans le même espace mémoire. |
Ptr | Accepté un segment et un déplacement à partir d'une adresse, et renverrait un pointeur vers cette adresse. Cela a été modifié dans la bibliothèque d'exécution, elle renvoie désormais simplement le déplacement. |
memw et mem | Ces tableaux donnaient accès à la mémoire DOS. Free Pascal les prend en charge sur la plateforme go32v2, ils sont cartographiés dans l'espace mémoire DOS. Vous avez besoin de l'unité go32 pour cela. Sur d'autres plateformes, ils ne sont pas pris en charge. |
Il ne faut pas utiliser ces fonctions, car elles sont très peu portables, elles sont spécifiques au DOS et au microprocesseur 80x86. Le compilateur Free Pascal est conçu pour être portable sur d'autres plates-formes, vous devez donc garder votre code aussi portable que possible et non spécifique au système. Autrement dit, à moins que vous n'écriviez des unités de pilote, bien sûr.
MemAvail et MaxAvail
Les anciennes fonctions MemAvail et MaxAvail de Turbo Pascal ne sont plus disponibles dans Free Pascal à partir de la version 2.0. La raison de cette incompatibilité est la suivante :
Sur les systèmes d'exploitation modernes, l'idée de «Mémoire libre disponible» n'est pas valable pour une application. Les raisons sont :
- Un cycle de microprocesseur après qu'une application ait demandé au système d'exploitation quelle quantité de mémoire est libre, une autre application peut avoir tout alloué.
- Ce que signifie «mémoire libre» n'est pas clair : inclut-il la mémoire d'échange, inclut-il la mémoire cache disque (le cache disque peut augmenter et diminuer sur les systèmes d'exploitation modernes), inclut-il la mémoire allouée à d'autres applications mais pouvant être échangé,...
Par conséquent, les programmes utilisant les fonctions MemAvail et MaxAvail devraient être réécrits afin qu'ils n'utilisent plus ces fonctions, car cela n'a plus de sens sur les systèmes d'exploitation modernes. Il y a 3 possibilités :
- Utilisez des exceptions pour détecter les erreurs de mémoire insuffisante.
- Définissez la variable globale "ReturnNilIfGrowHeapFails" sur True et vérifiez après chaque allocation si le pointeur est différent de NIL.
- Ne vous en souciez pas et déclarez une fonction factice appelée MaxAvail renvoyant toujours High(LongInt) (ou une autre constante).