Fuite de mémoire

WhiteHippo Messages postés 1154 Date d'inscription samedi 14 août 2004 Statut Membre Dernière intervention 5 avril 2012 - 26 janv. 2006 à 21:21
WhiteHippo Messages postés 1154 Date d'inscription samedi 14 août 2004 Statut Membre Dernière intervention 5 avril 2012 - 28 janv. 2006 à 01:28
Tout est parti d'une simple et toute petite application console écrite pour tester la fonction GetHeapStatus en vue de l'intégrer dans un futur programme :


program Episode1; // La Menace (de la mémoire) Fantome
{$APPTYPE CONSOLE}
uses
SysUtils;
var
I64 : Int64;
begin
Writeln( 'Memoire libre : ' + IntToStr(GetHeapStatus.TotalFree) + ' octets.' ) ;
// Code
I64 := $7FFFFFFFFFFFFFFF ;
Writeln( 'Max I64 : ' + IntToStr( I64 ) ) ;
Writeln( 'Memoire libre : ' + IntToStr(GetHeapStatus.TotalFree) + ' octets.' ) ;
// Attente Appui d'une touche
Readln;
end.


En sortie on obtient alors :


Memoire libre : 14112 octets.
Max I64 : 9223372036854775807
Memoire libre : 13972 octets.


Soit une différence de 140 octets !!!! Mais où ont-ils pu bien passer ?


Bon, afin de creuser l'affaire, j'ai alors utilisé deux variables de plus MemoireDebut et MemoireFin pour calculer la perte mémoire.


program Episode2; // L'attaque des clones
{$APPTYPE CONSOLE}
uses
SysUtils;
var
I64 : Int64;
MemoireDebut : Integer ;
MemoireFin : Integer ;
begin
MemoireDebut : = GetHeapStatus.TotalFree;
Writeln( 'Memoire libre : ' + IntToStr(MemoireDebut) + ' octets.' ) ;
I64 := $7FFFFFFFFFFFFFFF ;
Writeln( 'Max I64 : ' + IntToStr( I64 ) ) ;
MemoireFin := GetHeapStatus.TotalFree;
Writeln( 'Memoire libre : ' + IntToStr(MemoireFin) + ' octets.' ) ;
Writeln( 'Fuite memoire de ' + IntToStr( MemoireDebut - MemoireFin ) + ' octets.' ) ;
// Attente Appui d'une touche
Readln;
end.


En sortie on obtient alors :


Memoire libre : 14112 octets.
Max I64 : 9223372036854775807
Memoire libre : 13972 octets.
Fuite memoire de 140 octets.


Jusque là tout semble cohérent, mais faisons une lègère modification au code, en regroupant les fonctions d'affichage à la fin de celui-ci :


program Episode3; // La revanche des Sith
{$APPTYPE CONSOLE}
uses
SysUtils;
var
I64 : Int64;
MemoireDebut : Integer ;
MemoireFin : Integer ;
begin
MemoireDebut := GetHeapStatus.TotalFree;
// Code
I64 := $7FFFFFFFFFFFFFFF ;
MemoireFin := GetHeapStatus.TotalFree;
Writeln( 'Memoire libre : ' + IntToStr(MemoireDebut) + ' octets.' ) ;
Writeln( 'Max I64 : ' + IntToStr( I64 ) ) ;
Writeln( 'Memoire libre : ' + IntToStr(MemoireFin) + ' octets.' ) ;
Writeln( 'Fuite memoire de ' + IntToStr( MemoireDebut - MemoireFin ) + ' octets.' ) ;
// Attente Appui d'une touche
Readln;
end.


En sortie on obtient alors :


Memoire libre : 14112 octets.
Max I64 : 9223372036854775807
Memoire libre : 14112 octets.
Fuite memoire de 0 octets.


Il n'y a plus de fuite mémoire !!
Mais est-ce vraiment sur ?
Bon, là il y a vraiment de quoi se poser encore plus de questions!!


En analysant les différences entre les deux programmes Episode2 et Episode3, que peut-on en déduire ?


Une des 2 fonctions (writeln ou IntToStr) pire voir les deux utiliseraient de la mémoire à notre insu !!


program Episode4; // Un nouvel Espoir
{$APPTYPE CONSOLE}
uses
SysUtils;
var
I64 : Int64;
MemoireDebut : Integer ;
MemoireFin : Integer ;
begin
MemoireDebut := GetHeapStatus.TotalFree;
// Code
I64 := $7FFFFFFFFFFFFFFF ;
Writeln( 'Est-ce la fonction writeln?' ) ;
MemoireFin := GetHeapStatus.TotalFree;
Writeln( 'Fuite memoire de ' + IntToStr( MemoireDebut - MemoireFin ) + ' octets.' ) ;
// Attente Appui d'une touche
Readln;
end.


En sortie on obtient alors :


Est-ce la fonction writeln?
Fuite memoire de 0 octets.


Une fonction innocentée.


program Episode5; // L'Empire [des fuites mémoire] Contre-Attaque
{$APPTYPE CONSOLE}
uses
SysUtils;
var
I64 : Int64;
MemoireDebut : Integer ;
MemoireFin : Integer ;
begin
MemoireDebut := GetHeapStatus.TotalFree;
// Code
I64 := $7FFFFFFFFFFFFFFF ;
IntToStr(I64);
MemoireFin := GetHeapStatus.TotalFree;
Writeln( 'Fuite memoire de ' + IntToStr( MemoireDebut - MemoireFin ) + ' octets.' ) ;
// Attente Appui d'une touche
Readln;
end.


En sortie on obtient alors :


Fuite memoire de 32 octets.


Nous avons à présent notre coupable. Mais pourquoi avons nous perdu 32 octets ??? (et même plus étant donné que l'on rappelle IntToStr à la fin pour afficher la fuite de mémoire!!)
Peut on éviter cette fuite ?


Etant déjà à l'épisode 5, je n'aurais certainement pas de mal à moi aussi atteindre l'épisode 6, mais aller jusqu'à finir par un grand dénouement, là va pas falloir m'en demander trop ;)


program Episode6; // Le retour du Jedi
{$APPTYPE CONSOLE}
uses
SysUtils;
var
I64 : Int64;
MemoireDebut : Integer ;
MemoireFin : Integer ;

procedure AfficheEntier ( const s : string ; i : int64 ) ;
begin
Writeln( S + IntToStr( i ) ) ;
end ;

begin
MemoireDebut : = GetHeapStatus.TotalFree;
// Code
I64 := $7FFFFFFFFFFFFFFF ;
AfficheEntier( 'Max I64 : ', I64 ) ;
MemoireFin := GetHeapStatus.TotalFree;
AfficheEntier( 'Fuite memoire : Octets perdus = ', MemoireDebut - MemoireFin ) ;
// Attente Appui d'une touche
Readln;
end.


En sortie on obtient alors :


Max I64 : 9223372036854775807
Fuite memoire : Octets perdus = 0


Et voilou, jeune Padawan !


Mais au fait pourquoi, est ce que cela fonctionne à présent ?


Justement j'attends vos commentaires à ce propos même si j'ai ma petite idée là-dessus.



Cordialement.


<HR>
Il existe 10 catégories de personne. Ceux qui connaissent le binaire et les autres...

3 réponses

Cirec Messages postés 3833 Date d'inscription vendredi 23 juillet 2004 Statut Modérateur Dernière intervention 18 septembre 2022 50
26 janv. 2006 à 22:33
Salut,
Très très interressant tous ça.
il semblerait que la fonction IntToStr alloue de la mémoire pour pouvoir faire le transtypage et que le fait d'utiliser une procedure à part pour effectuer l'affichage permet de la libérer de suite comme toute autre variable déclaré localement dans une procédure où fonction.

oh grand maitre toi qui a le savoir éclaire ma lanterne est ce que le jeune padawan est sur la voie de la sagesse

@+
Cirec
0
f0xi Messages postés 4205 Date d'inscription samedi 16 octobre 2004 Statut Modérateur Dernière intervention 12 mars 2022 35
27 janv. 2006 à 16:26
aprés quelques tests :

// Et les aventuriers de la memoire perdue
var
I64 : Int64;
MemoireDebut,
MemoireFin : Integer ;
begin
MemoireDebut := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireDebut]));
I64 := $7FFFFFFFFFFFFFFF ;
writeln(format('Max I64 : %d octets ',[I64]));
MemoireFin := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireFin]));
writeln(format('Fuite memoire : %d octets ',[MemoireDebut - MemoireFin]));
Readln;
end.

fuite de 96 octet seulement grace a format (vive l'assembleur!)

<hr size="2" width="100%">
// La revenche des fuiths
var
I64 : Int64;
MemoireDebut,
MemoireFin : Integer ;
begin
MemoireDebut := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireDebut]));
I64 := $7FFFFFFFFFFFFFFF ;
writeln(format('Max I64 : %d octets ',[sizeof(I64)]));
MemoireFin := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireFin]));
writeln(format('Fuite memoire : %d octets ',[MemoireDebut - MemoireFin]));
Readln;
end.

Fuite de 76 octets (gain de 20 octets ...)

normal, la chaine renvoyée passe de 19 caracteres a 1 caracteres !

Int64 ne fait pas quelques milliard d'octets mais bien 8 octet seulement (sizeof!)

<hr size="2" width="100%">
var
I64 : Int64;
MemoireDebut,
MemoireFin : Integer ;
begin
MemoireDebut := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireDebut]));
writeln(format('Max I64 : %d',[high(I64)]));
writeln(format('Taille I64 : %d octets ',[sizeof(I64)]));
MemoireFin := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireFin]));
writeln(format('Fuite memoire : %d octets ',[MemoireDebut - MemoireFin]));
Readln;
end.

fuite de 128 octets, normal je rajoute une ligne qui fait 13+19 caracteres... sant compter les Eoln en plus (2 octet)

en gros, la fuite pourrait etre expliquée par la taille des chaines generée entre MemoireDebut et MemoireFin...

<hr size="2" width="100%">begin
MemoireDebut := GetHeapStatus.TotalFree;
writeln(format('%d octets',[MemoireDebut]));
writeln(format('%d',[high(I64)]));
writeln(format('%d octets',[sizeof(I64)]));
MemoireFin := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireFin]));
writeln(format('Fuite memoire : %d octets ',[MemoireDebut - MemoireFin]));
Readln;
end.

begin
MemoireDebut := GetHeapStatus.TotalFree;
writeln(inttostr(MemoireDebut)+' octets');
writeln(inttostr(high(I64)));
writeln(inttostr(sizeof(I64))+' octets');
MemoireFin := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireFin]));
writeln(format('Fuite memoire : %d octets ',[MemoireDebut - MemoireFin]));
Readln;
end.

fuites de 84 octets dans les deux cas ...

begin
MemoireDebut := GetHeapStatus.TotalFree;
writeln('12345 octets');
writeln('1234567890123456789');
writeln('1 octets');
MemoireFin := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireFin]));
writeln(format('Fuite memoire : %d octets ',[MemoireDebut - MemoireFin]));
Readln;
end.

fuites de 0 octets ... normal en fait

pas d'erreur, c'est bien le fait de l'allocation dynamique de chaine qui provoque la difference de memoire.

en gros quand delphi alloue une chaine suffisement grande pour la convertion de type.

<hr size="2" width="100%">
pour eviter la fuite :

procedure showinfos;
begin
writeln(format('%d octets',[MemoireDebut]));
writeln(format('%d',[high(I64)]));
writeln(format('%d octets',[sizeof(I64)]));
end;

begin
MemoireDebut := GetHeapStatus.TotalFree;
Showinfos;
MemoireFin := GetHeapStatus.TotalFree;
writeln(format('Memoire libre : %d octets ',[MemoireFin]));
writeln(format('Fuite memoire : %d octets ',[MemoireDebut - MemoireFin]));
Readln;
end.

normal, car a la fin d'une fonction ou procedure, la memoire allouée pour le traitement est libérée.

<hr size="2" width="100%">La theorie c'est quand on sait tout, mais que rien ne fonctionne.
La pratique c'est quand tout fonctionne, mais que personne ne sait pourquoi.
<hr>
0
WhiteHippo Messages postés 1154 Date d'inscription samedi 14 août 2004 Statut Membre Dernière intervention 5 avril 2012 3
28 janv. 2006 à 01:28
Cirec, foxi, je suis en grande partie d'accord avec vous, tout est affaire de portée (Durée de vie) de variable MAIS ... (et oui il y a un MAIS )

Que la fonction IntToStr réserve des octets pour y stocker le résultat de la conversion, rien de plus logique. Rien à redire de ce côté là. Par contre, ce qui n'est pas normal, c'est que le compilateur ne libère pas les chaines allouées hors des fonctions et procédures lorqu'elle ne sont plus nécessaires, c'est à dire lorsque leur portée n'est plus

Pour être plus clair, prenons un exemple :

Writeln( 'Memoire libre : ' + IntToStr(MemoireDebut) + ' octets.' ) ;

Portée de la chaine : Le temps de l'appel de la fonction Writeln, et c'est tout.

Hors, on s'aperçoit que la chaine de retour de IntToStr est toujours présente en mémoire, alors qu'elle ne sera plus jamais utilisée par le programme.

JE SUPPOSE qu'elle sera certainement supprimée à la fin du programme (portée globale), mais ce n'est pas normal.

L'appel de IntToStr au sein d'une procédure ou d'une fonction résoud le problème (la portée étant clairement définie, celle de la procédure ou de la fonction, bref une portée locale) Le compilateur libère donc à la fin de la fonction, les variables allouées localement.

Une autre façon de s'en convaincre, c'est d'appeller 20 fois :

Writeln( 'Memoire libre : ' + IntToStr(MemoireDebut) + ' octets.' ) ;

dans un premier temps à l'aide d'une boucle

for i:=1 to 20 do
begin
Writeln( 'Memoire libre : ' + IntToStr(MemoireDebut) + ' octets.' ) ;
end ;

et dans un second temps en multipliant les appels :

Writeln( 'Memoire libre : ' + IntToStr(MemoireDebut) + ' octets.' ) ;
Writeln( 'Memoire libre : ' + IntToStr(MemoireDebut) + ' octets.' ) ;
Writeln( 'Memoire libre : ' + IntToStr(MemoireDebut) + ' octets.' ) ;
Writeln( 'Memoire libre : ' + IntToStr(MemoireDebut) + ' octets.' ) ;
// ... etc 20 fois

Dans le second cas, on s'apperçoit, que la fuite de mémoire augmente (et c'est peu dire lorsque l'on voit la quantité de mémoire non libérée : 800 octets = 20 * 40 octets) en fonction du nombre d'appel, ce qui n'est pas le cas de la boucle, dont la fuite reste à 40 octets. (Au passage, on remarquera qu'il y a quand même 40 octets perdus )

Là encore, JE SUPPOSE que le compilateur a limité la portée de la chaine à l'intérieur de la boucle, ce qu'il n'a pas su faire dans le cas du multi appel.

Il me semble que c'est une mauvaise gestion de la libération de la mémoire allouée en fonction de la portée, et donc un bug.

Bien sur, ce sont MES SUPPOSITIONS.

Cordialement.


<HR>
Il existe 10 catégories de personne. Ceux qui connaissent le binaire et les autres...
0
Rejoignez-nous