web-dev-qa-db-fra.com

Est-il jamais OK de ne pas utiliser free () sur la mémoire allouée?

J'étudie l'ingénierie informatique et j'ai des cours d'électronique. J'ai entendu, par deux de mes professeurs (de ces cours) qu'il est possible d'éviter d'utiliser la fonction free() (après malloc(), calloc(), etc.) car les espaces mémoire alloués ne seront probablement plus utilisés pour allouer une autre mémoire. C'est-à-dire, par exemple, si vous allouez 4 octets puis les libérez, vous aurez 4 octets d'espace qui ne seront probablement plus alloués: vous aurez un tro.

Je pense que c'est fou: vous ne pouvez pas avoir un not-toy-program où vous allouez de la mémoire sur le tas sans le libérer. Mais je n'ai pas les connaissances nécessaires pour expliquer exactement pourquoi il est si important que pour chaque malloc() il y ait un free().

Donc: existe-t-il des circonstances dans lesquelles il pourrait être approprié d'utiliser une malloc() sans utiliser free()? Et sinon, comment expliquer cela à mes professeurs?

83
Nick

Facile: il suffit de lire la source de n'importe quelle implémentation semi-sérieuse de malloc()/free(). Par cela, je veux dire le gestionnaire de mémoire qui gère le travail des appels. Cela peut être dans la bibliothèque d'exécution, la machine virtuelle ou le système d'exploitation. Bien sûr, le code n'est pas également accessible dans tous les cas.

S'assurer que la mémoire n'est pas fragmentée, en joignant des trous adjacents dans des trous plus grands, est très très courant. Des répartiteurs plus sérieux utilisent des techniques plus sérieuses pour garantir cela.

Supposons donc que vous effectuez trois allocations et désallocations et que les blocs soient disposés en mémoire dans cet ordre:

+-+-+-+
|A|B|C|
+-+-+-+

La taille des allocations individuelles n'a pas d'importance. puis vous libérez le premier et le dernier, A et C:

+-+-+-+
| |B| |
+-+-+-+

quand vous libérez enfin B, vous (au début, du moins en théorie) vous retrouvez avec:

+-+-+-+
| | | |
+-+-+-+

qui peut être fragmenté en seulement

+-+-+-+
|     |
+-+-+-+

c'est-à-dire un seul bloc libre plus grand, aucun fragment ne reste.

Références, comme demandé:

  • Essayez de lire le code pour dlmalloc . Je suis beaucoup plus avancé, étant une implémentation complète de qualité de production.
  • Même dans les applications embarquées, des implémentations de défragmentation sont disponibles. Voir par exemple ces notes sur le heap4.c code dans FreeRTOS .
100
unwind

Les autres réponses expliquent déjà parfaitement que les implémentations réelles de malloc() et free() coalescent en effet (défragmentent) les trous en plus gros morceaux libres. Mais même si ce n'était pas le cas, ce serait quand même une mauvaise idée de renoncer à free().

Le fait est que votre programme vient d’allouer (et veut libérer) ces 4 octets de mémoire. Si elle doit s'exécuter pendant une période de temps prolongée, il est fort probable qu'elle devra à nouveau allouer seulement 4 octets de mémoire. Ainsi, même si ces 4 octets ne fusionnent jamais en un espace contigu plus grand, ils peuvent toujours être réutilisés par le programme lui-même.

42
Angew

C'est un non-sens total, par exemple, il existe de nombreuses implémentations différentes de malloc, certains essaient de rendre le tas plus efficace comme Doug Lea's ou this one.

10
Paul Evans

Vos professeurs travaillent-ils avec POSIX, par hasard? S'ils sont habitués à écrire beaucoup de petites applications Shell minimalistes, c'est un scénario où je peux imaginer que cette approche ne serait pas trop mauvaise - libérer tout le tas à la fois au loisir de l'OS est plus rapide que de libérer mille variables. Si vous vous attendez à ce que votre application s'exécute pendant une seconde ou deux, vous pouvez facilement vous en sortir sans aucune allocation.

C'est toujours une mauvaise pratique bien sûr (les améliorations de performances doivent toujours être basées sur le profilage, pas un vague sentiment d'intestin), et ce n'est pas quelque chose que vous devriez dire aux étudiants sans expliquer les autres contraintes, mais je peux imaginer beaucoup de shell-piping minuscule -applications à écrire de cette façon (si vous n'utilisez pas l'allocation statique pure et simple). Si vous travaillez sur quelque chose qui profite de la non-libération de vos variables, vous travaillez soit dans des conditions d'extrême faible latence (dans ce cas, comment pouvez-vous même vous permettre une allocation dynamique et C++?: D), ou vous êtes faire quelque chose de très, très mal (comme allouer un tableau d'entiers en allouant un millier d'entiers l'un après l'autre plutôt qu'un seul bloc de mémoire).

9
Luaan

Vous avez mentionné qu'il s'agissait de professeurs d'électronique. Ils peuvent être utilisés pour écrire des micrologiciels/logiciels en temps réel, étant en mesure de chronométrer avec précision l'exécution de quelque chose est souvent nécessaire. Dans ces cas, le fait de savoir que vous disposez de suffisamment de mémoire pour toutes les allocations et de ne pas libérer et réallouer de la mémoire peut donner une limite de temps d'exécution plus facile à calculer.

Dans certains schémas, la protection matérielle de la mémoire peut également être utilisée pour s'assurer que la routine se termine dans sa mémoire allouée ou génère un piège dans ce qui devrait être très cas exceptionnels.

5
Paddy3118

Vos professeurs ne se trompent pas, mais ils le sont aussi (ils sont au moins trompeurs ou simplistes). La fragmentation de la mémoire pose des problèmes de performances et d'utilisation efficace de la mémoire, vous devez donc parfois y penser et prendre des mesures pour l'éviter. Une astuce classique est, si vous allouez beaucoup de choses qui sont de la même taille, saisir un pool de mémoire au démarrage qui est un multiple de cette taille et gérer son utilisation entièrement en interne, garantissant ainsi que la fragmentation ne se produit pas à la Niveau du système d'exploitation (et les trous dans votre mappeur de mémoire interne auront exactement la bonne taille pour le prochain objet de ce type qui se présente).

Il existe des bibliothèques tierces entières qui ne font que gérer ce genre de choses pour vous, et parfois c'est la différence entre des performances acceptables et quelque chose qui fonctionne beaucoup trop lentement. malloc() et free() prennent un temps considérable à exécuter, que vous commencerez à remarquer si vous les appelez souvent.

Ainsi, en évitant naïvement d'utiliser malloc() et free(), vous pouvez éviter à la fois la fragmentation et les problèmes de performances - mais lorsque vous allez droit au but, vous devez toujours vous assurer que vous free() tout ce que vous malloc() sauf si vous avez une très bonne raison de faire autrement. Même lorsque vous utilisez un pool de mémoire interne, une bonne application free() la mémoire du pool avant de se fermer. Oui, le système d'exploitation le nettoiera, mais si le cycle de vie de l'application est modifié plus tard, il serait facile d'oublier que le pool traîne toujours ...

Les applications de longue durée doivent bien sûr être très scrupuleuses quant au nettoyage ou au recyclage de tout ce qu'elles ont alloué, ou elles finissent par manquer de mémoire.

2
Matthew Walton

Je pense que l'affirmation énoncée dans la question est absurde si elle est prise littéralement du point de vue du programmeur, mais elle a la vérité (au moins une partie) du point de vue du système d'exploitation.

malloc () finira par appeler mmap () ou sbrk () qui récupérera une page du système d'exploitation.

Dans tout programme non trivial, les chances que cette page soit rendue au système d'exploitation pendant la durée de vie d'un processus sont très faibles, même si vous libérez () la majeure partie de la mémoire allouée. La mémoire libre () ne sera donc disponible pour le même processus la plupart du temps, mais pas pour les autres.

2
mfro

En prenant cela sous un angle différent de celui des commentateurs et réponses précédents, une possibilité est que vos professeurs aient eu de l'expérience avec des systèmes où la mémoire a été allouée statiquement (c'est-à-dire lorsque le programme a été compilé).

L'allocation statique survient lorsque vous faites des choses comme:

define MAX_SIZE 32
int array[MAX_SIZE];

Dans de nombreux systèmes temps réel et embarqués (les plus susceptibles d'être rencontrés par les EE ou les CE), il est généralement préférable d'éviter complètement l'allocation dynamique de mémoire. Ainsi, les utilisations de malloc, new et de leurs équivalents de suppression sont rares. En plus de cela, la mémoire des ordinateurs a explosé ces dernières années.

Si vous disposez de 512 Mo et que vous allouez statiquement 1 Mo, vous avez environ 511 Mo à parcourir avant que votre logiciel n'explose (enfin, pas exactement ... mais suivez-moi ici). En supposant que vous avez 511 Mo à abuser, si vous malloc 4 octets chaque seconde sans les libérer, vous pourrez exécuter pendant près de 73 heures avant de manquer de mémoire. Étant donné que de nombreuses machines s'arrêtent une fois par jour, cela signifie que votre programme ne manquera jamais de mémoire!

Dans l'exemple ci-dessus, la fuite est de 4 octets par seconde, soit 240 octets/min. Imaginez maintenant que vous réduisez ce rapport octet/min. Plus ce ratio est faible, plus votre programme peut s'exécuter sans problème. Si vos malloc sont peu fréquents, c'est une réelle possibilité.

Heck, si vous savez que vous n'allez à malloc quelque chose qu'une seule fois, et que malloc ne sera plus jamais frappé, alors cela ressemble beaucoup à l'allocation statique, bien que vous n'ayez pas besoin de savoir la taille de ce que vous allouez à l'avance. Par exemple: Disons que nous avons à nouveau 512 Mo. Nous devons malloc 32 tableaux d'entiers. Ce sont des entiers typiques - 4 octets chacun. Nous savons que les tailles de ces tableaux ne dépasseront jamais 1024 entiers. Aucune autre allocation de mémoire ne se produit dans notre programme. Avons-nous assez de mémoire? 32 * 1024 * 4 = 131 072. 128 Ko - alors oui. Nous avons beaucoup d'espace. Si nous savons que nous n'allouerons plus de mémoire, nous pouvons en toute sécurité malloc ces tableaux sans les libérer. Cependant, cela peut également signifier que vous devez redémarrer la machine/l'appareil si votre programme plante. Si vous démarrez/arrêtez votre programme 4096 fois, vous allouerez les 512 Mo. Si vous avez des processus zombies, il est possible que la mémoire ne soit jamais libérée, même après un crash.

Sauvez-vous de la douleur et de la misère, et utilisez ce mantra comme The One Truth: malloc devrait toujours être associé à un free. new devrait toujours avoir un delete.

2
Shaz

Je suis surpris que personne n'ait encore cité The Book :

Cela peut ne pas être vrai finalement, car les mémoires peuvent devenir suffisamment volumineuses pour qu'il soit impossible de manquer de mémoire libre pendant la durée de vie de l'ordinateur. Par exemple, il y a environ 3 × 1013 microsecondes en un an, donc si nous devions contre une fois par microseconde nous aurions besoin d'environ 1015 cellules de mémoire pour construire une machine qui pourrait fonctionner pendant 30 ans sans manquer de mémoire. Cette quantité de mémoire semble absurdement grande par rapport aux normes actuelles, mais ce n'est pas physiquement impossible. D'un autre côté, les processeurs deviennent plus rapides et un futur ordinateur peut avoir un grand nombre de processeurs fonctionnant en parallèle sur une seule mémoire, il peut donc être possible d'utiliser la mémoire beaucoup plus rapidement que nous l'avions postulé.

http://sarabander.github.io/sicp/html/5_002e3.xhtml#FOOT298

Ainsi, en effet, de nombreux programmes peuvent très bien fonctionner sans jamais se soucier de libérer de la mémoire.

1
oakad

Vos professeurs soulèvent un point important. Malheureusement, l'utilisation de l'anglais est telle que je ne suis pas absolument sûr de ce qu'ils ont dit. Permettez-moi de répondre à la question en termes de programmes non-jouets qui ont certaines caractéristiques d'utilisation de la mémoire et avec lesquels j'ai personnellement travaillé.

Certains programmes se comportent bien. Ils allouent la mémoire par vagues: beaucoup d'allocations de petite ou moyenne taille suivies de beaucoup de libres, en cycles répétitifs. Dans ces programmes, les allocateurs de mémoire typiques fonctionnent plutôt bien. Ils fusionnent les blocs libérés et à la fin d'une vague, la majeure partie de la mémoire libre se trouve dans de gros morceaux contigus. Ces programmes sont assez rares.

La plupart des programmes se comportent mal. Ils allouent et désallouent la mémoire plus ou moins au hasard, dans une variété de tailles allant de très petite à très grande, et ils conservent une utilisation élevée des blocs alloués. Dans ces programmes, la capacité de fusionner des blocs est limitée et au fil du temps, ils finissent avec une mémoire très fragmentée et relativement non contiguë. Si l'utilisation totale de la mémoire dépasse environ 1,5 Go dans un espace mémoire de 32 bits et qu'il existe des allocations de (disons) 10 Mo ou plus, l'une des allocations importantes échouera éventuellement. Ces programmes sont courants.

D'autres programmes libèrent peu ou pas de mémoire jusqu'à ce qu'ils s'arrêtent. Ils allouent progressivement de la mémoire pendant l'exécution, ne libérant que de petites quantités, puis s'arrêtent, moment auquel toute la mémoire est libérée. Un compilateur est comme ça. Il en va de même pour une machine virtuelle. Par exemple, le runtime .NET CLR, lui-même écrit en C++, ne libère probablement jamais de mémoire. Pourquoi cela?

Et c'est la réponse finale. Dans les cas où le programme utilise suffisamment de mémoire, la gestion de la mémoire à l'aide de malloc et de free n'est pas une réponse suffisante au problème. À moins que vous n'ayez la chance d'avoir affaire à un programme bien comporté, vous devrez concevoir un ou plusieurs allocateurs de mémoire personnalisés qui pré-alloueront de gros morceaux de mémoire, puis sous-allouer selon la stratégie de votre choix. Vous ne pouvez pas utiliser gratuitement du tout, sauf lorsque le programme s'arrête.

Sans savoir exactement ce que vos professeurs ont dit, pour des programmes vraiment à l'échelle de la production, je sortirais probablement de leur côté.

ÉDITER

Je vais essayer de répondre à certaines des critiques. Évidemment SO n'est pas un bon endroit pour des articles de ce type. Juste pour être clair: j'ai environ 30 ans d'expérience dans l'écriture de ce type de logiciel, y compris quelques compilateurs. Je n'ai pas de références académiques , juste mes propres ecchymoses. Je ne peux pas m'empêcher de ressentir les critiques de la part de personnes ayant une expérience beaucoup plus étroite et plus courte.

Je répète mon message clé: équilibrer malloc et free est pas une solution suffisante pour l'allocation de mémoire à grande échelle dans les programmes réels. La coalescence des blocs est normale et prend du temps, mais c'est pas suffisant. Vous avez besoin d'allocateurs de mémoire sérieux et intelligents, qui ont tendance à saisir la mémoire en morceaux (en utilisant malloc ou autre) et à libérer rarement. C'est probablement le message que les professeurs d'OP avaient en tête, qu'il a mal compris.

1
david.pfx

Je connais un cas où la libération de mémoire explicite est pire qu'inutile. Autrement dit, lorsque vous avez besoin de toutes vos données jusqu'à la fin de la durée de vie du processus. En d'autres termes, leur libération n'est possible que juste avant la fin du programme. Étant donné que tout système d'exploitation moderne prend soin de libérer de la mémoire à la fin d'un programme, appeler free() n'est pas nécessaire dans ce cas. En fait, cela peut ralentir l'arrêt du programme, car il peut avoir besoin d'accéder à plusieurs pages en mémoire.

1
Miklós Homolya