Si j'ai besoin d'utiliser un morceau de mémoire tout au long de la vie de mon programme, est-il vraiment nécessaire de le libérer juste avant la fin du programme?

66

Dans de nombreux livres et tutoriels, j'ai entendu parler de la gestion de la mémoire et de certaines pratiques mystérieuses et terribles si je ne libérais pas la mémoire après l'avoir utilisée.

Je ne peux pas parler pour d’autres systèmes (même s’il est raisonnable de supposer qu’ils adoptent une pratique similaire), mais au moins sous Windows, le noyau est en principe garanti de nettoyer la plupart des ressources (à l’exception d’un petit nombre impair) utilisées par un programme après la fin du programme. Qui comprend la mémoire de tas, entre autres choses.

Je comprends pourquoi vous voudriez fermer un fichier après l’avoir utilisé pour le mettre à la disposition de l’utilisateur ou pourquoi vous voudriez déconnecter un socket connecté à un serveur afin de gagner de la bande passante, mais cela semble ridicule. avoir à gérer tout votre mémoire utilisée par votre programme.

Maintenant, je suis d' accord que cette question est large depuis la façon dont vous devez gérer votre mémoire est basée sur la quantité de mémoire dont vous avez besoin et quand vous en avez besoin, je vais donc limiter la portée de cette question à ceci: Si je dois utiliser un morceau de mémoire tout au long de la vie de mon programme, est-il vraiment nécessaire de le libérer juste avant la fin du programme?

Edit: La question suggérée comme un doublon était spécifique à la famille de systèmes d'exploitation Unix. Sa première réponse spécifiait même un outil spécifique à Linux (par exemple, Valgrind). Cette question vise à couvrir la plupart des systèmes d'exploitation "normaux" non intégrés et explique pourquoi il est recommandé ou non de libérer de la mémoire si nécessaire tout au long de la vie d'un programme.

CaptainOvvious
la source
19
Vous avez marqué cette langue comme agnostique, mais dans de nombreuses langues (par exemple, Java), il n’est pas nécessaire de libérer manuellement la mémoire; cela se produit automatiquement quelque temps après que la dernière référence à un objet ait disparu
Richard Tingle
3
et bien sûr, vous pouvez écrire en C ++ sans aucune suppression et tout irait bien pour mémoire
Nikko
5
@RichardTingle Bien que je ne puisse penser à aucune autre langue que C et peut-être même C ++, la balise indépendante de la langue était destinée à couvrir toutes les langues ne disposant pas de nombreux utilitaires de récupération des ordures. Cela est certes rare de nos jours cependant. Vous pourriez soutenir que vous pouvez maintenant implémenter un tel système en C ++, mais vous avez toujours le choix de ne pas supprimer de mémoire.
CaptainObvious
4
Vous écrivez: "le noyau est fondamentalement garanti pour nettoyer toutes les ressources [...] après la fin du programme". Ceci est généralement faux, car tout n'est pas mémoire, un descripteur ou un objet du noyau. Mais c'est vrai pour la mémoire, à laquelle vous limitez immédiatement votre question.
Eric Towers

Réponses:

107

Si j'ai besoin d'utiliser un morceau de mémoire tout au long de la vie de mon programme, est-il vraiment nécessaire de le libérer juste avant la fin du programme?

Ce n'est pas obligatoire, mais cela peut avoir des avantages (ainsi que des inconvénients).

Si le programme alloue de la mémoire une fois pendant son temps d'exécution et ne la libérait jamais avant la fin du processus, il peut être judicieux de ne pas libérer la mémoire manuellement et de ne pas s'appuyer sur le système d'exploitation. Sur tous les systèmes d’exploitation modernes que je connais, c’est sûr, à la fin du processus, toute la mémoire allouée est restituée de manière fiable au système.
Dans certains cas, le fait de ne pas nettoyer explicitement la mémoire allouée peut même être nettement plus rapide que de procéder au nettoyage.

Cependant, en libérant explicitement toute la mémoire en fin d’exécution,

  • lors du débogage / test, les outils de détection de fuite de mémoire ne vous montreront pas de "faux positifs"
  • il pourrait être beaucoup plus facile de déplacer le code qui utilise la mémoire avec l'allocation et la désallocation dans un composant séparé et de l'utiliser ultérieurement dans un contexte différent où le temps d'utilisation de la mémoire doit être contrôlé par l'utilisateur du composant

La durée de vie des programmes peut changer. Aujourd'hui, votre programme est peut-être un petit utilitaire de ligne de commande, avec une durée de vie moyenne de moins de 10 minutes, et alloue de la mémoire par portions de quelques kb toutes les 10 secondes - vous n'avez donc pas besoin de libérer la mémoire allouée avant la fin du programme. Plus tard, le programme est modifié et obtient une utilisation étendue dans le cadre d’un processus serveur d’une durée de vie de plusieurs semaines. Ne libérez donc pas de mémoire inutilisée entre ces options. Sinon, votre programme consommera progressivement toute la mémoire disponible du serveur. . Cela signifie que vous devrez revoir l'ensemble du programme et ajouter du code de désaffectation par la suite. Si vous avez de la chance, la tâche est facile, sinon, il se peut que vous ayez de grandes chances de rater une place. Et quand vous serez dans cette situation, vous voudrez avoir ajouté le "gratuit"

Plus généralement, l'écriture d'allocation et de code de désallocation associé est toujours considérée comme une "bonne habitude" par de nombreux programmeurs: en faisant cela toujours, vous réduisez la probabilité d'oubli du code de désallocation dans des situations où la mémoire doit être libérée.

Doc Brown
la source
12
Bon point sur le développement de bonnes habitudes de programmation.
Lawrence
4
En général, je suis fortement d'accord avec ce conseil. Garder de bonnes habitudes avec la mémoire est très important. Cependant, je pense qu'il y a des exceptions. Par exemple, si vous avez alloué un graphe fou qui prendra plusieurs secondes à parcourir et à se libérer "correctement", vous améliorerez l'expérience utilisateur en mettant simplement fin au programme et en laissant le système d'exploitation le supprimer.
GrandOpener
1
Il est sûr sur tout système d'exploitation moderne "normal" (non intégré avec protection de la mémoire) et constituerait un grave problème s'il ne l'était pas. Si un processus non privilégié peut perdre définitivement de la mémoire que le système d'exploitation ne récupérera pas, son exécution répétée peut entraîner une insuffisance de mémoire du système. La santé à long terme du système d'exploitation ne peut pas dépendre du manque de bogues dans les programmes non privilégiés. (Bien sûr, cela ignore des éléments comme les segments de mémoire partagée Unix, qui sont au mieux sauvegardés par un espace d'échange.)
Peter Cordes
7
@GrandOpener Dans ce cas, vous préférerez peut-être utiliser un type d'allocateur basé sur la région pour cet arbre, afin de pouvoir l'allouer normalement et simplement désallouer la région entière d'un coup, le moment venu, au lieu de le parcourir et de le libérer. ça petit à petit. C'est toujours "correct".
Thomas
3
En outre, si la mémoire doit réellement exister pendant toute la durée de vie du programme, il peut être judicieux de créer une structure sur la pile, par exemple, dans main.
Kyle Strand
11

La libération de mémoire à la fin de l'exécution d'un programme n'est qu'une perte de temps CPU. C'est comme ranger une maison avant de la sortir de son orbite.

Cependant, un programme de courte durée peut parfois devenir un programme beaucoup plus long. Libérer des choses devient alors nécessaire. Si cela n'a pas été envisagé au moins dans une certaine mesure, cela peut impliquer un remaniement important.

Une solution intelligente à cela est "talloc" qui vous permet de faire une charge d'allocations de mémoire et de les jeter ensuite en un seul appel.

Peter Green
la source
1
En supposant que vous sachiez que votre système d'exploitation n'a pas ses propres fuites.
WGroleau
5
"perte de temps CPU" Bien que très, très peu de temps. freeest généralement beaucoup plus rapide que malloc.
Paul Draper
@PaulDraper Aye, perdre du temps à tout rééchanger est beaucoup plus important.
Déduplicateur
5

Vous pouvez utiliser une langue avec garbage collection (telle que Scheme, Ocaml, Haskell, Common Lisp et même Java, Scala, Clojure).

(Dans la plupart des langages GC-ed, il n’existe aucun moyen de libérer explicitement et manuellement la mémoire! Parfois, certaines valeurs peuvent être finalisées , par exemple, le GC et le système d’exécution feraient fermer une valeur de descripteur de fichier lorsque cette valeur est inaccessible; bien sûr, non fiable, et vous devriez plutôt fermer explicitement vos descripteurs de fichiers, car la finalisation n'est jamais garantie)

Vous pouvez également, pour votre programme codé en C (ou même C ++), utiliser le récupérateur de déchets conservateur de Boehm . Vous remplaceriez alors tous vos mallocobjets GC_malloc et ne vous freesoucieriez plus d'aucun pointeur. Bien sûr, vous devez comprendre les avantages et les inconvénients de l’utilisation du GC de Boehm. Lisez aussi le manuel du GC .

La gestion de la mémoire est une propriété globale d'un programme. D'une certaine manière, il (et la vivacité de certaines données) est non-compositionnel et non modulaire, puisqu'il s'agit de la propriété d'un programme entier.

Enfin, comme d'autres l'ont souligné, la freecréation de zones de mémoire C allouées au segment de disque est une bonne pratique. Pour un programme jouet n'allouant pas beaucoup de mémoire, vous pouvez même décider de ne pas utiliser de freemémoire du tout (car une fois le processus terminé, ses ressources, y compris son espace d'adressage virtuel , seraient libérées par le système d'exploitation).

Si j'ai besoin d'utiliser un morceau de mémoire tout au long de la vie de mon programme, est-il vraiment nécessaire de le libérer juste avant la fin du programme?

Non, vous n'avez pas besoin de faire ça. Et de nombreux programmes du monde réel ne se soucient pas de libérer de la mémoire nécessaire sur toute la durée de vie (en particulier, le compilateur GCC ne libère pas une partie de sa mémoire). Cependant, lorsque vous faites cela (par exemple, vous ne vous souciez pas de freecertaines données allouées dynamiquement en C ), vous feriez mieux de commenter ce fait pour faciliter le travail des futurs programmeurs sur le même projet. J'ai tendance à recommander que la quantité de mémoire non libérée reste limitée et qu'elle soit relativement petite par rapport à la mémoire totale utilisée.

Notez que freesouvent le système ne libère pas de mémoire dans le système d’exploitation (par exemple, en appelant munmap (2) sur les systèmes POSIX), mais marque généralement une zone mémoire comme étant réutilisable par la suite malloc. En particulier, l'espace d'adressage virtuel (comme vu /proc/self/mapsdans Linux, voir proc (5) ...) pourrait ne pas être réduit après free(d'où des utilitaires comme psou topqui signalent la même quantité de mémoire utilisée par votre processus).

Basile Starynkevitch
la source
3
"Parfois, certaines valeurs peuvent être finalisées, par exemple le GC et le système d'exécution ferment une valeur de descripteur de fichier lorsque cette valeur est inaccessible" Vous pouvez souligner que, dans la plupart des langages GCed, rien ne garantit qu'une mémoire sera jamais récupérée, n'importe quel finaliseur exécuté, même en sortie: stackoverflow.com/questions/7880569/…
Déduplicateur
Personnellement, je préfère cette réponse, car elle nous encourage à utiliser GC (approche plus automatisée).
Ta Thanh Dinh
3
@tathanhdinh Plus automatisé, mais exclusivement pour la mémoire. Complètement manuel, pour toutes les autres ressources. GC travaille en échangeant déterminisme et mémoire pour un traitement pratique de la mémoire. Et non, les finaliseurs n'aident pas beaucoup et ont leurs propres problèmes.
Déduplicateur
3

Ce n'est pas nécessaire , car vous ne manquerez pas d'exécuter correctement votre programme si vous échouez. Cependant, il y a des raisons pour lesquelles vous pouvez choisir de le faire, si vous en avez l'occasion.

L'un des cas les plus puissants que je rencontre (encore et encore) est que quelqu'un écrit un petit morceau de code de simulation qui s'exécute dans leur exécutable. Ils disent "nous aimerions que ce code soit intégré à la simulation". Je leur demande ensuite comment ils envisagent de se réinitialiser entre les courses de monte-carlo et ils me regardent sans rien comprendre. "Que voulez-vous dire par réinitialisation? Vous venez d'exécuter le programme avec de nouveaux paramètres?"

Parfois, un nettoyage en profondeur facilite grandement l’utilisation de votre logiciel. Dans le cas de nombreux exemples, vous présumez que vous n’avez jamais besoin de nettoyer quelque chose et vous faites des hypothèses sur la façon dont vous pouvez gérer les données et leur durée de vie autour de ces hypothèses. Lorsque vous passez dans un nouvel environnement où ces présomptions ne sont pas valides, des algorithmes entiers risquent de ne plus fonctionner.

Pour illustrer à quel point des choses étranges peuvent devenir étranges, examinez la manière dont les langages gérés gèrent la finalisation à la fin des processus ou la manière dont C # gère l’arrêt par programmation des domaines d’application. Ils se lient en nœuds parce qu'il y a des hypothèses qui tombent à l'eau dans ces cas extrêmes.

Cort Ammon
la source
1

Ignorer les langues où vous ne libérez pas de mémoire manuellement de toute façon ...

Ce que vous considérez comme "un programme" actuellement pourrait à un moment donné devenir simplement une fonction ou une méthode faisant partie d’un programme plus vaste. Et puis cette fonction peut être appelée plusieurs fois. Et puis la mémoire que vous devriez avoir "libérée manuellement" sera une fuite de mémoire. C'est bien sûr un jugement.

gnasher729
la source
2
cela semble simplement répéter le point (et bien mieux expliqué) de la réponse la plus haute postée quelques heures auparavant: "il pourrait être beaucoup plus facile de déplacer le code qui utilise la mémoire avec l'allocation et la désallocation dans un composant séparé et de l'utiliser ultérieurement dans un contexte différent où le temps d'utilisation de la mémoire doit être contrôlé par l'utilisateur du composant ... "
gnat
1

Parce qu'il est très probable que vous souhaitiez modifier votre programme dans un certain temps, peut-être l'intégrer à autre chose, exécuter plusieurs instances en séquence ou en parallèle. Il deviendra alors nécessaire de libérer manuellement cette mémoire - mais vous ne vous souviendrez plus des circonstances et cela vous coûtera beaucoup plus de temps pour ré-comprendre votre programme.

Faites les choses pendant que votre compréhension à leur sujet est encore fraîche.

C'est un petit investissement qui peut générer de gros rendements dans le futur.

Agent_L
la source
2
cela ne semble rien ajouter de substantiel aux points soulevés et expliqués dans les 11 réponses précédentes. En particulier, nous avons déjà évoqué trois ou quatre fois la possibilité de modifier le programme à l’avenir
Gnat
1
@gnat De manière alambiquée et obscure - oui. Dans une déclaration claire - non. Concentrons-nous sur la qualité des réponses, pas sur la rapidité.
Agent_L
1
En ce qui concerne la qualité, la meilleure réponse semble être bien meilleure pour expliquer ce point
gnat
1

Non, ce n'est pas nécessaire, mais c'est une bonne idée.

Vous dites que vous pensez que "des choses mystérieuses et terribles se produiraient si je ne libérais pas la mémoire une fois l'utilisation terminée".

Sur le plan technique, la seule conséquence est que votre programme continuera à consommer plus de mémoire jusqu'à ce qu'une limite stricte soit atteinte (par exemple, votre espace d'adressage virtuel est épuisé) ou que les performances deviennent inacceptables. Si le programme est sur le point de se terminer, rien de tout cela n'a d'importance, car le processus cesse effectivement d'exister. Les "choses mystérieuses et terribles" concernent uniquement l'état mental du développeur. Trouver la source d'une fuite de mémoire peut être un cauchemar absolu (c'est un euphémisme) et il faut beaucoup de talent et de discipline pour écrire du code sans fuites. L'approche recommandée pour développer cette compétence et cette discipline consiste à toujours libérer de la mémoire dès qu'elle n'est plus nécessaire, même si le programme est sur le point de se terminer.

Bien entendu, cela présente l’avantage supplémentaire que votre code peut être réutilisé et adapté plus facilement, comme d’autres l’ont dit.

Cependant , il existe au moins un cas où il est préférable de ne pas libérer de mémoire juste avant la fin du programme.

Prenons le cas où vous avez effectué des millions de petites allocations et qui ont généralement été permutées sur disque. Lorsque vous commencez à tout libérer, la plus grande partie de votre mémoire doit être reconvertie en RAM afin de pouvoir accéder aux informations de comptabilité et les supprimer immédiatement. Cela peut rendre le programme prendre quelques minutes juste pour sortir! Sans parler de mettre beaucoup de pression sur le disque et la mémoire physique pendant ce temps. Si la mémoire physique est insuffisante pour commencer (le programme est peut-être fermé car un autre programme consomme beaucoup de mémoire), il peut alors être nécessaire d’échanger plusieurs pages individuelles lorsque plusieurs objets doivent être libérés de la mémoire. même page, mais ne sont pas libérés consécutivement.

Si, au lieu de cela, le programme abandonne simplement, le système d'exploitation supprimera simplement toute la mémoire qui a été échangée sur le disque, ce qui est presque instantané car il ne nécessite aucun accès au disque.

Il est important de noter que dans un langage OO, l'appel du destructeur d'un objet forcera également la mémoire à être échangée; Si vous devez le faire, vous pouvez également libérer la mémoire.

Artelius
la source
1
Pouvez-vous citer quelque chose pour soutenir l'idée selon laquelle la mémoire paginée doit être paginée pour être désallouée? C'est évidemment inefficace, les systèmes d'exploitation modernes sont certainement plus intelligents que cela!
1
@Jon of All Trades, les systèmes d'exploitation modernes sont intelligents, mais paradoxalement, la plupart des langues modernes ne le sont pas. Lorsque vous libérez (quelque chose), le compilateur mettra "quelque chose" sur la pile, puis appellera free (). Pour le placer sur la pile, vous devez le replacer dans la RAM s'il est remplacé.
Jeffiekins
@JonofAllTrades une liste libre aurait cet effet, c'est une implémentation assez obsolète de malloc. Mais le système d'exploitation ne peut vous empêcher d'utiliser une telle implémentation.
0

Les principales raisons de nettoyer manuellement sont les suivantes: vous êtes moins enclin à confondre une fuite de mémoire véritable avec un bloc de mémoire qui sera nettoyé à la sortie, vous rencontrez parfois des bogues dans l'allocateur uniquement lorsque vous parcourez l'intégralité de la structure de données. désallouer, et vous aurez beaucoup plus petit mal de tête si vous refactoring jamais de sorte que certains objets ne doivent se désallouée avant la fin du programme.

Les principales raisons de ne pas nettoyer manuellement sont les suivantes: performances, performances, performances et possibilité qu'un bogue free-two ou use-after-free apparaisse dans votre code de nettoyage inutile, ce qui peut se transformer en bogue d'arrêt ou en sécurité exploit.

Si vous vous souciez de la performance, vous voulez toujours définir votre profil pour savoir où vous perdez tout votre temps, au lieu de deviner. Un compromis possible consiste à envelopper votre code de nettoyage optionnel dans un bloc conditionnel, à le laisser lors du débogage pour bénéficier des avantages de l'écriture et du débogage du code, et ensuite, si et seulement si vous avez déterminé de manière empirique que c'est trop. overhead, dites au compilateur de le sauter dans votre exécutable final. Et un délai dans la clôture du programme est, presque par définition, rarement dans le chemin critique.

Davislor
la source