Pourquoi «free» dans C ne prend-il pas le nombre d’octets à libérer?

Juste pour être clair: je sais que malloc et free sont implémentés dans la bibliothèque C, qui alloue généralement des morceaux de mémoire à partir du système d’exploitation et fait sa propre gestion pour répartir de plus petites quantités de mémoire dans l’application et en suivre le nombre. octets alloués. Cette question n’est pas comment libre sait combien libérer .

Je veux plutôt savoir pourquoi le free été fait de cette façon en premier lieu. En tant que langage de bas niveau, je pense qu’il serait tout à fait raisonnable de demander à un programmeur C de garder une trace non seulement de la mémoire allouée mais aussi de la quantité (en fait, je constate que je garde le suivi du nombre d’octets malloced de toute façon). Il me semble aussi que donner explicitement le nombre d’octets à free pourrait permettre certaines optimisations de performances, par exemple un allocateur qui a des pools séparés pour différentes tailles d’allocation serait capable de déterminer de quel pool se libérer en regardant simplement les arguments en entrée, et il y aurait moins de frais généraux d’espace.

Donc, en bref, pourquoi malloc et freefree -ils été créés pour être tenus de garder une trace interne du nombre d’octets alloués? Est-ce juste un accident historique?

Une petite modification: quelques personnes ont fourni des points tels que “et si vous libérez un montant différent de ce que vous avez alloué”. Mon API imaginée pourrait simplement nécessiter de libérer exactement le nombre d’octets alloués; libérer plus ou moins pourrait simplement être UB ou mise en œuvre définie. Je ne veux cependant pas décourager la discussion sur d’autres possibilités.

Un argument free(void *) (introduit dans Unix V7) présente un autre avantage majeur par rapport au mfree(void *, size_t) antérieur à deux arguments mfree(void *, size_t) que je n’ai pas vu ici: un argument free simplifie considérablement toutes les autres API qui fonctionnent avec mémoire de tas. Par exemple, si free nécessitait la taille du bloc mémoire, alors strdup devrait en quelque sorte renvoyer deux valeurs (pointeur + taille) au lieu d’un (pointeur), et C rend les retours à valeurs multiples beaucoup plus encombrants que les retours à valeur unique. Au lieu de char *strdup(char *) il faudrait écrire char *strdup(char *, size_t *) ou struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *) struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *) struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *) . (De nos jours, cette seconde option est plutôt tentante, car nous soaps que les chaînes terminées par NUL sont le “bogue de conception le plus catastrophique de l’histoire de l’informatique” , mais avec le recul nécessaire. char * était en fait considéré comme un avantage décisif par rapport à des concurrents comme Pascal et Algol .) De plus, ce problème ne se limite pas à strdup – il affecte toutes les fonctions système ou définies par l’utilisateur qui allouent de la mémoire de tas.

Les premiers concepteurs d’Unix étaient des gens très intelligents, et il y a beaucoup de raisons pour lesquelles free est meilleur que mfree donc je pense que la réponse à la question est qu’ils ont remarqué cela et ont conçu leur système en conséquence. Je doute que vous trouviez une trace directe de ce qui se passait dans leur tête au moment où ils ont pris cette décision. Mais on peut imaginer.

Imaginez que vous écrivez des applications en C pour fonctionner sur Unix V6, avec son mfree deux arguments. Vous vous êtes bien débrouillé jusqu’à présent, mais le suivi de ces tailles de pointeurs devient de plus en plus difficile à mesure que vos programmes deviennent plus ambitieux et requièrent de plus en plus l’utilisation de variables de tas. Mais alors vous avez une idée géniale: au lieu de copier autour de ces size_t s tout le temps, vous pouvez simplement écrire des fonctions utilitaires, qui cachent la taille directement dans la mémoire allouée:

 void *my_alloc(size_t size) { void *block = malloc(sizeof(size) + size); *(size_t *)block = size; return (void *) ((size_t *)block + 1); } void my_free(void *block) { block = (size_t *)block - 1; mfree(block, *(size_t *)block); } 

Et plus vous écrivez du code en utilisant ces nouvelles fonctions, plus elles sont impressionnantes. Non seulement ils rendent votre code plus facile à écrire, mais ils accélèrent également votre code, deux choses qui ne vont pas souvent ensemble! Avant que vous ne size_t ces size_t s partout, ce qui ajoutait des frais supplémentaires au processeur pour la copie et signifiait que vous deviez renvoyer des registres plus souvent (en particulier pour les arguments de fonction supplémentaires) et gaspiller de la mémoire (car les appels de fonctions nesteds Il en résulte que plusieurs copies de size_t sont stockées dans différents frameworks de stack). Dans votre nouveau système, vous devez toujours dépenser la mémoire pour stocker la size_t , mais une seule fois, et elle ne sera jamais copiée nulle part. Celles-ci peuvent sembler peu efficaces, mais gardez à l’esprit que nous parlons de machines haut de gamme avec 256 Ko de RAM.

Cela vous rend heureux! Vous partagez donc votre astuce avec les hommes barbus qui travaillent sur la prochaine version d’Unix, mais cela ne les rend pas heureux, cela les rend sortingstes. Vous voyez, ils étaient juste en train d’append un tas de nouvelles fonctions utilitaires comme strdup , et ils réalisent que les personnes utilisant votre astuce ne pourront pas utiliser leurs nouvelles fonctions, car leurs nouvelles fonctions utilisent toutes le pointeur encombrant + API de taille. Et puis cela vous rend sortingste aussi, car vous réalisez que vous devrez réécrire vous-même la bonne fonction strdup(char *) dans chaque programme que vous écrivez, au lieu de pouvoir utiliser la version du système.

Mais attendez! Nous sums en 1977 et la rétrocompatibilité ne sera pas inventée avant 5 ans! Et d’ailleurs, personne de sérieux n’utilise réellement cette chose “Unix” obscure avec son nom hors-couleur. La première édition de K & R est en cours pour l’éditeur, mais ce n’est pas un problème – il est dit tout de suite que “C ne fournit aucune opération pour traiter directement des objects composites tels que des chaînes de caractères … il n’y a pas de tas … ” À ce stade de l’histoire, ssortingng.h et malloc sont des extensions de fournisseur (!). Donc, suggère Bearded Man # 1, nous pouvons les changer comme bon nous semble; Pourquoi ne pas simplement déclarer que votre allocateur est l’allocateur officiel ?

Quelques jours plus tard, Bearded Man # 2 voit la nouvelle API et dit bon, attendez, c’est mieux qu’avant, mais il passe toujours un mot entier par allocation pour stocker la taille. Il considère cela comme la prochaine chose au blasphème. Tout le monde le regarde comme s’il était fou, car que pouvez-vous faire d’autre? Ce soir-là, il rest en retard et invente un nouvel allocateur qui ne stocke pas la taille du tout, mais l’infère à la volée en effectuant des changements de magie noirs sur la valeur du pointeur et en le maintenant en place. La nouvelle API signifie que personne ne remarque le commutateur, mais ils remarquent que le lendemain matin, le compilateur utilise 10% de RAM en moins.

Et maintenant, tout le monde est content: vous obtenez votre code plus facile à écrire et plus rapide, Bearded Man # 1 peut écrire une belle mise en page simple que les gens vont utiliser, et Bearded Man # 2 – confiant – retourne à jouer avec quines . Expédier!

Ou du moins, c’est comme ça que ça aurait pu arriver.

“Pourquoi free in C ne prend-il pas le nombre d’octets à libérer?”

Parce qu’il n’y a pas besoin de ça, et que ça n’aurait pas vraiment de sens de toute façon.

Lorsque vous allouez quelque chose, vous voulez indiquer au système le nombre d’octets à allouer (pour des raisons évidentes).

Cependant, lorsque vous avez déjà alloué votre object, la taille de la région de mémoire que vous récupérez est maintenant déterminée. C’est implicite. C’est un bloc de mémoire contigu. Vous ne pouvez pas désallouer une partie de celui-ci (oublions realloc() , ce n’est pas ce qu’il fait de toute façon), vous ne pouvez que libérer tout le contenu. Vous ne pouvez pas “désallouer des octets X” – vous libérez le bloc de mémoire que vous avez obtenu de malloc() ou vous ne le faites pas.

Et maintenant, si vous voulez le libérer, vous pouvez simplement dire au système de gestion de mémoire: “voici ce pointeur, free() le bloc vers lequel il pointe.” – et le gestionnaire de mémoire saura comment le faire, soit parce qu’il connaît implicitement la taille, soit parce qu’il n’a peut-être même pas besoin de la taille.

Par exemple, la plupart des implémentations typiques de malloc() maintiennent une liste liée de pointeurs vers des blocs de mémoire libres et alloués. Si vous passez un pointeur sur free() , il vous suffira de rechercher ce pointeur dans la liste “atsortingbuée”, de dissocier le nœud correspondant et de le joindre à la liste “free”. Il n’avait même pas besoin de la taille de la région. Il n’aura besoin que de ces informations lorsqu’il tentera potentiellement de réutiliser le bloc en question.

C peut ne pas être aussi “abstrait” que C ++, mais il est toujours destiné à être une abstraction par rapport à l’assemblage. À cette fin, les détails les plus bas sont retirés de l’équation. Cela vous évite d’avoir à gérer l’alignement et le remplissage, ce qui rendrait tous vos programmes C non portables.

En bref, c’est tout l’intérêt d’écrire une abstraction .

En fait, dans l’ancien allocateur de mémoire du kernel Unix, mfree() prenait un argument de size . malloc() et mfree() conservé deux tableaux (un pour la mémoire principale, un autre pour le swap) contenant des informations sur les adresses et les tailles des blocs libres.

Il n’y avait pas d’allocateur d’espace utilisateur jusqu’à Unix V6 (les programmes utiliseraient simplement sbrk() ). Dans Unix V6, iolib incluait un allocateur avec alloc(size) et un appel free() qui ne prenait pas d’argument de taille. Chaque bloc de mémoire était précédé de sa taille et d’un pointeur sur le bloc suivant. Le pointeur n’était utilisé que sur les blocs libres, lors de la lecture de la liste libre, et était réutilisé comme mémoire de bloc sur les blocs en cours d’utilisation.

Dans Unix 32V et Unix V7, cela a été remplacé par une nouvelle implémentation de malloc() et free() , où free() ne prenait pas d’argument de size . L’implémentation était une liste circulaire, chaque bloc était précédé d’un mot contenant un pointeur vers le bloc suivant et un bit “occupé” (alloué). Donc, malloc()/free() n’a même pas suivi de taille explicite.

Pourquoi free in C ne prend-il pas le nombre d’octets à libérer?

Parce que ce n’est pas nécessaire Les informations sont déjà disponibles dans la gestion interne effectuée par malloc / free.

Voici deux considérations (qui peuvent ou non avoir consortingbué à cette décision):

  • Pourquoi voudriez-vous qu’une fonction reçoive un paramètre dont elle n’a pas besoin?

    (Cela compliquerait virtuellement tous les codes clients reposant sur la mémoire dynamic et appendait une redondance complètement inutile à votre application). Garder une trace de l’allocation des pointeurs est déjà un problème difficile. Le suivi des allocations de mémoire et des tailles associées augmenterait inutilement la complexité du code client.

  • Que ferait la fonction free modifiée dans ces cas?

     void * p = malloc(20); free(p, 25); // (1) wrong size provided by client code free(NULL, 10); // (2) generic argument mismatch 

    Est-ce que ça ne serait pas gratuit (causer une fuite de mémoire?)? Ignorer le deuxième paramètre? Arrêtez l’application en appelant exit? Mettre en œuvre cela appendait des points de défaillance supplémentaires dans votre application, pour une fonctionnalité dont vous n’avez probablement pas besoin (et si vous en avez besoin, consultez mon dernier point ci-dessous – “Implémenter une solution au niveau de l’application”).

Je veux plutôt savoir pourquoi le libre a été fait de cette façon en premier lieu.

Parce que c’est la façon “correcte” de le faire. Une API doit exiger les arguments nécessaires pour effectuer son opération, et pas plus que cela .

Il me semble aussi que donner explicitement le nombre d’octets à libérer pourrait permettre certaines optimisations de performances, par exemple un allocateur qui a des pools séparés pour différentes tailles d’allocation serait capable de déterminer de quel pool se libérer en regardant simplement les arguments en entrée, et il y aurait moins de frais généraux d’espace.

Les moyens appropriés pour le mettre en œuvre sont les suivants:

  • (au niveau système) dans l’implémentation de malloc – rien n’empêche l’implémenteur de la bibliothèque d’écrire malloc pour utiliser différentes stratégies en interne, en fonction de la taille reçue.

  • (au niveau de l’application) en encapsulant malloc et free dans vos propres API, et en les utilisant à la place (partout dans votre application dont vous pourriez avoir besoin).

Cinq raisons viennent à l’esprit:

  1. C’est pratique. Il supprime toute une charge de surcharge du programmeur et évite une classe extrêmement difficile à suivre.

  2. Cela ouvre la possibilité de libérer une partie d’un bloc. Mais comme les gestionnaires de mémoire veulent généralement avoir des informations de suivi, ce que cela signifie?

  3. La légèreté des courses en orbite est une priorité pour le rembourrage et l’alignement. La nature de la gestion de la mémoire signifie que la taille réelle atsortingbuée est très probablement différente de celle que vous avez demandée. Cela signifie que si vous free besoin d’une taille et d’un emplacement, malloc devrait être modifié pour renvoyer également la taille réelle allouée.

  4. Il n’est pas clair qu’il y ait un avantage réel à faire passer la taille, de toute façon. Un gestionnaire de mémoire type comporte 4-16 octets d’en-tête pour chaque bloc de mémoire, y compris la taille. Cet en-tête de bloc peut être commun pour la mémoire allouée et non allouée et lorsque des blocs adjacents sont libérés, ils peuvent être regroupés. Si vous faites en sorte que l’appelant stocke la mémoire libre, vous pouvez libérer 4 octets par bloc en ne disposant pas d’un champ de taille distinct dans la mémoire allouée, mais ce champ n’est probablement pas gagné car l’appelant doit le stocker quelque part. Mais maintenant, cette information est dispersée dans la mémoire plutôt que d’être située de manière prévisible dans le bloc d’en-tête, ce qui risque d’être de toute façon moins efficace sur le plan opérationnel.

  5. Même si c’était plus efficace, il est fort peu probable que votre programme passe beaucoup de temps à libérer de la mémoire, de sorte que les avantages seraient minimes.

Incidemment, votre idée concernant des allocateurs séparés pour différents éléments de taille est facilement implémentée sans ces informations (vous pouvez utiliser l’adresse pour déterminer où l’allocation s’est produite). Ceci est fait systématiquement en C ++.

Ajouté plus tard

Une autre réponse, plutôt ridicule, a montré que std :: allocator était la preuve que free pouvait fonctionner de cette façon mais, en fait, cela montre bien pourquoi free ne fonctionne pas de cette façon. Il y a deux différences principales entre ce que malloc / free do et ce que fait std :: allocator. Tout d’abord, malloc et free sont orientés vers l’utilisateur – ils sont conçus pour les programmeurs généraux – alors que std::allocator est conçu pour fournir une allocation de mémoire spécialisée à la bibliothèque standard. Cela donne un bon exemple du moment où le premier de mes points n’a pas ou n’a pas d’importance. Comme il s’agit d’une bibliothèque, les difficultés liées à la complexité de la taille du suivi sont de toute façon cachées à l’utilisateur.

Deuxièmement, std :: allocator fonctionne toujours avec le même élément de taille, ce qui signifie qu’il est possible d’utiliser le nombre d’éléments transmis à l’origine pour déterminer la quantité de données gratuites. Pourquoi cela diffère du free lui-même est illustratif. Dans std::allocator les éléments à allouer sont toujours de la même taille, connus, de la même taille et toujours du même type. Ils ont donc toujours le même type d’alignement. Cela signifie que l’allocateur pourrait être spécialisé pour allouer simplement un tableau de ces éléments au début et les dissortingbuer au besoin. Vous ne pouvez pas le faire avec free car il n’ya aucun moyen de garantir que la meilleure taille à renvoyer correspond à la taille demandée, mais il est beaucoup plus efficace de renvoyer parfois des blocs plus gros que ceux demandés par l’appelant * et le gestionnaire doit suivre la taille exacte effectivement accordée. Transmettre ces types de détails d’implémentation à l’utilisateur est un casse-tête inutile qui ne procure aucun avantage à l’appelant.

– * Si quelqu’un a encore du mal à comprendre ce point, considérez ceci: un allocateur de mémoire typique ajoute une petite quantité d’informations de suivi au début d’un bloc de mémoire, puis renvoie un décalage de pointeur. Les informations stockées ici incluent généralement des pointeurs vers le prochain bloc libre, par exemple. Supposons que l’en-tête ne soit long que de 4 octets (ce qui est en réalité plus petit que la plupart des librairies réelles) et n’inclut pas la taille, alors imaginons un bloc libre de 20 octets lorsque l’utilisateur demande un bloc de 16 octets. Le système renverrait le bloc de 16 octets, mais laisserait un fragment de 4 octets qui ne pourrait jamais être utilisé pour perdre du temps à chaque appel de malloc . Si, à la place, le gestionnaire renvoie simplement le bloc de 20 octets, il enregistre ces fragments en désordre et peut allouer plus proprement la mémoire disponible. Mais si le système doit le faire correctement sans suivre la taille elle-même, nous demandons à l’utilisateur de suivre – pour chaque allocation unique – la quantité de mémoire réellement allouée si elle doit la renvoyer gratuitement. Le même argument s’applique au remplissage pour les types / allocations qui ne correspondent pas aux limites souhaitées. Donc, tout au plus, exiger que la taille soit free est (a) complètement inutile puisque l’allocateur de mémoire ne peut pas compter sur la taille passée pour correspondre à la taille réellement allouée ou (b) oblige l’utilisateur à faire le suivi du réel taille qui serait facilement manipulée par n’importe quel gestionnaire de mémoire sensible.

Je ne fais que poster ceci comme une réponse, non pas parce que c’est ce que vous espérez, mais parce que je pense que c’est la seule qui soit plausiblement correcte:

C’était probablement jugé commode à l’origine, et il n’a pas pu être amélioré par la suite.
Il n’y a probablement aucune raison convaincante pour cela. (Mais je supprimerai volontiers ceci si cela est montré incorrect.)

Il y aurait des avantages si c’était possible: vous pouviez allouer un seul gros morceau de mémoire dont vous connaissiez la taille à l’avance, puis le libérer un peu à la fois, plutôt que d’affecter et de libérer de petits blocs de mémoire à plusieurs resockets. Actuellement, des tâches comme celle-ci ne sont pas possibles.


Pour les nombreux (beaucoup 1 !) D’entre vous qui pensent que passer la taille est tellement ridicule:

Puis-je vous référer à la décision de conception de C ++ pour la méthode std::allocator::deallocate ?

 void deallocate(pointer p, size_type n); 

Tous les n T objects dans la zone pointée par p doivent être détruits avant cet appel.
n doit correspondre à la valeur transmise à allocate pour obtenir cette mémoire.

Je pense que vous aurez un temps plutôt “intéressant” pour parsingr cette décision de conception.


En ce qui concerne l’ operator delete , il s’avère que la proposition N3778 de 2013 (“C ++ Sized Deallocation”) est également destinée à résoudre ce problème.


Il suffit de regarder les commentaires sous la question d’origine pour voir combien de personnes ont fait des affirmations précipitées telles que “la taille demandée est complètement inutile pour l’appel free pour justifier l’absence du paramètre de size .

malloc et free vont de pair, chaque “malloc” étant associé à un “free”. Ainsi, il est tout à fait logique que l’appariement “libre” d’un “malloc” précédent libère simplement la quantité de mémoire allouée par ce malloc – c’est le cas d’utilisation majoritaire qui aurait du sens dans 99% des cas. Imaginez toutes les erreurs de mémoire si toutes les utilisations de malloc / free par tous les programmeurs à travers le monde nécessitaient que le programmeur garde une trace de la quantité allouée dans malloc, et puis rappelez-vous de les libérer. Le scénario dont vous parlez doit utiliser plusieurs mallocs / frees dans une implémentation de gestion de la mémoire.

Je pense que c’est parce qu’il est très pratique de ne pas avoir à suivre manuellement les informations de taille de cette manière (dans certains cas) et également moins sujettes aux erreurs du programmeur.

En outre, realloc aurait besoin de ces informations comptables, qui, je pense, contiennent plus que la taille de l’allocation. c’est-à-dire qu’il permet de définir le mécanisme par lequel il fonctionne.

Vous pourriez écrire votre propre allocateur qui a quelque peu fonctionné comme vous le suggérez et cela se fait souvent en c ++ pour les allocateurs de pool de manière similaire pour des cas spécifiques (avec des gains de performances potentiellement importants) bien que cela soit généralement implémenté nouveau pour l’allocation de blocs de pool.

Je ne vois pas comment un allocateur fonctionnerait sans suivre la taille de ses allocations. S’il ne le faisait pas, comment pourrait-il savoir quelle mémoire est disponible pour satisfaire une future demande de malloc ? Il doit au moins stocker une sorte de structure de données contenant des adresses et des longueurs, pour indiquer où se trouvent les blocs de mémoire disponibles. (Et bien sûr, stocker une liste d’espaces libres équivaut à stocker une liste d’espaces alloués).

Eh bien, la seule chose dont vous avez besoin est un pointeur que vous utiliserez pour libérer la mémoire que vous avez précédemment allouée. Le nombre d’octets est géré par le système d’exploitation, vous n’avez donc pas à vous en soucier. Il ne serait pas nécessaire d’obtenir le nombre d’octets alloués renvoyés par free (). Je vous suggère un moyen manuel de compter le nombre d’octets / positions alloués par un programme en cours d’exécution:

If you work in Linux and you want to know the amount of bytes/positions malloc has allocated, you can make a simple program that uses malloc once or n times and prints out the pointers you get. In addition, you must make the program sleep for a few seconds (enough for you to do the following). After that, run that program, look for its PID, write cd /proc/process_PID and just type “cat maps”. The output will show you, in one specific line, both the beginning and the final memory addresses of the heap memory region (the one in which you are allocating memory dinamically).If you print out the pointers to these memory regions being allocated, you can guess how much memory you have allocated.

J’espère que cela aide!

Pourquoi devrait-il? malloc() and free() are intentionally very simple memory management primitives , and higher-level memory management in C is largely up to the developer. T

Moreover realloc() does that already – if you reduce the allocation in realloc() is it will not move the data, and the pointer returned will be the the same as the original.

It is generally true of the entire standard library that it is composed of simple primitives from which you can build more complex functions to suit your application needs. So the answer to any question of the form “why does the standard library not do X” is because it cannot do everything a programmer might think of (that’s what programmers are for), so it chooses to do very little – build your own or use third-party libraries. If you want a more extensive standard library – including more flexible memory management, then C++ may be the answer.

You tagged the question C++ as well as C, and if C++ is what you are using, then you should hardly be using malloc/free in any case – apart from new/delete, STL container classes manage memory automatically, and in a manner likely to be specifically appropriate to the nature of the various containers.