web-dev-qa-db-fra.com

Pourquoi «free» en C ne prend 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 petits lots de mémoire à l'application et conserve une trace du nombre d'octets alloués. Cette question n'est pas Comment free sait-il combien à libérer .

Je veux plutôt savoir pourquoi free a été créé de cette façon en premier lieu. Étant un langage de bas niveau, je pense qu'il serait parfaitement raisonnable de demander à un programmeur C de garder une trace non seulement de la mémoire allouée mais de la quantité (en fait, je trouve généralement que je finis par garder une trace du nombre d'octets malloced de toute façon). Il me semble également 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 en mesure de déterminer de quel pool se libérer simplement en regardant les arguments d'entrée, et il y aurait moins d'espace en général.

Donc, en bref, pourquoi malloc et free ont-ils été créés de telle sorte qu'ils soient tenus de conserver en interne le nombre d'octets alloués? Est-ce juste un accident historique?

Une petite modification: quelques personnes ont fourni des points comme "et si vous libérez un montant différent de celui que vous avez alloué". Mon API imaginée pourrait simplement nécessiter une pour libérer exactement le nombre d'octets alloués; libérer plus ou moins pourrait simplement être défini par l'UB ou implémenté. Je ne veux cependant pas décourager la discussion sur d'autres possibilités.

82
jaymmer

Un argument free(void *) (introduit dans Unix V7) a un autre avantage majeur par rapport au précédent mfree(void *, size_t) à deux arguments que je n'ai pas vu mentionné ici: un argument free simplifie considérablement every other API qui fonctionne avec la mémoire de tas. Par exemple, si free avait besoin de la taille du bloc de mémoire, alors strdup devrait d'une manière ou d'une autre retourner deux valeurs (pointeur + taille) au lieu d'une (pointeur), et C crée des valeurs multiples retourne beaucoup plus lourd que les retours à valeur unique. Au lieu de char *strdup(char *), il faudrait écrire char *strdup(char *, size_t *) ou bien struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *). (De nos jours, cette deuxième option semble assez tentante, car nous savons que les chaînes terminées par NUL sont les "bogue de conception le plus catastrophique de l'histoire de l'informatique" , mais c'est du recul. De retour dans les années 70, la capacité de C gérer les chaînes comme un simple char * était en fait considéré comme un définissant un avantage sur des concurrents comme Pascal et ALGOL .) De plus, ce n'est pas seulement strdup qui en souffre problème - il affecte chaque fonction définie par le système ou par l'utilisateur qui alloue de la mémoire de tas.

Les premiers concepteurs d'Unix étaient des gens très intelligents, et il y a de nombreuses raisons pour lesquelles free est meilleur que mfree donc, fondamentalement, je pense que la réponse à la question est qu'ils l'ont remarqué et ont conçu leur système en conséquence. Je doute que vous trouverez un enregistrement direct 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 qu'elles s'exécutent sur V6 Unix, avec ses deux arguments mfree. Vous avez réussi jusqu'à présent, mais garder une trace de ces tailles de pointeurs devient de plus en plus compliqué à mesure que vos programmes deviennent plus ambitieux et nécessitent de plus en plus l'utilisation de variables allouées par 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 quelques 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 de code en utilisant ces nouvelles fonctions, plus elles semblent impressionnantes. Non seulement ils rendent votre code plus facile à écrire, ils aussi rendent votre code plus rapide - deux choses qui ne vont pas souvent ensemble! Avant de passer ces size_t Partout, ce qui ajoutait une surcharge CPU pour la copie et signifiait que vous deviez renverser les registres plus souvent (en particulier pour les arguments de fonction supplémentaires) et gaspiller la mémoire (depuis les appels de fonction imbriqués entraîneront souvent le stockage de plusieurs copies du size_t dans des cadres de pile différents). Dans votre nouveau système, vous devez toujours dépenser la mémoire pour stocker le size_t, Mais une seule fois, et il n'est jamais copié nulle part. Cela peut sembler être de petites efficacités, 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 super truc avec les hommes barbus qui travaillent sur la prochaine version d'Unix, mais cela ne les rend pas heureux, cela les rend tristes. Vous voyez, ils étaient juste en train d'ajouter un tas de nouvelles fonctions utilitaires comme strdup, et ils se rendent compte que les gens qui utilisent votre truc cool ne pourront pas utiliser leurs nouvelles fonctions, car leurs nouvelles fonctions utilisez le pointeur encombrant + API de taille. Et cela vous rend triste aussi, car vous vous rendez compte 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 sommes en 1977, et la rétrocompatibilité ne sera pas inventée avant 5 ans! Et d'ailleurs, personne de sérieux en fait - utilise cette chose "Unix" obscure avec son nom hors couleur. La première édition de K&R est en cours d'acheminement vers l'éditeur, mais ce n'est pas un problème - elle indique dès la première page que "C ne fournit aucune opération pour traiter directement avec des objets composites tels que des chaînes de caractères ... il n'y a pas de tas ... ". À ce stade de l'historique, string.h Et malloc sont des extensions de fournisseur (!). Donc, suggère Bearded Man # 1, nous pouvons les changer comme bon nous semble; pourquoi ne déclarons-nous pas simplement votre allocateur délicat comme étant 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 dépense toujours un mot entier par allocation stockant la taille. Il considère cela comme la prochaine étape du blasphème. Tout le monde le regarde comme s'il était fou, car que pouvez-vous faire d'autre? Cette nuit-là, il reste tard et invente un nouvel allocateur qui ne stocke pas du tout la taille, mais le déduit à la volée en effectuant des décalages de bits de magie noire sur la valeur du pointeur, et l'échange tout en gardant la nouvelle API en place. La nouvelle API signifie que personne ne remarque le changement, mais ils remarquent que le lendemain matin, le compilateur utilise 10% de RAM en moins.

Et maintenant tout le monde est heureux: vous obtenez votre code plus facile à écrire et plus rapide, Bearded Man # 1 arrive à écrire un joli simple strdup que les gens utiliseront réellement, et Bearded Man # 2 - confiant qu'il a gagné sa garde pour un peu - remonte à déconner avec quines . Expédier!

Ou du moins, c'est ainsi que cela s'est produit pourrait.

94
Nathaniel J. Smith

"Pourquoi free en C ne prend-il pas le nombre d'octets à libérer?"

Parce qu'il n'y a pas besoin de , et que n'aurait pas tout à fait du sens en tous cas.

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

Cependant, lorsque vous avez déjà alloué votre objet, la taille de la région 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 en désallouer une partie (oublions realloc(), ce n'est pas ce qu'il fait de toute façon ), vous ne pouvez désallouer que tout. Vous ne pouvez pas non plus "désallouer X octets" - 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 du gestionnaire de mémoire: "voici ce pointeur, free() le bloc vers lequel il pointe." - et le gestionnaire de mémoire saura le faire, soit parce qu'il connaît implicitement la taille, soit car il pourrait même ne pas avoir 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 à free(), il recherchera simplement ce pointeur dans la liste "allouée", dissociera le nœud correspondant et l'attachera à la liste "libre". Il n'avait même pas besoin de la taille de la région. Il n'aura besoin de ces informations que lorsqu'il tentera potentiellement de réutiliser le bloc en question.

En fait, dans l'ancien allocateur de mémoire du noyau Unix, mfree() a pris un argument size. malloc() et mfree() ont conservé deux tableaux (un pour la mémoire centrale, un autre pour l'échange) qui contenaient des informations sur les adresses et les tailles de bloc 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 a été précédé de sa taille et d'un pointeur sur le bloc suivant. Le pointeur a été utilisé uniquement sur les blocs libres, lors de la navigation dans la liste gratuite, et a été réutilisé comme mémoire de bloc sur les blocs en cours d'utilisation.

Sous Unix 32V et Unix V7, cela a été remplacé par une nouvelle implémentation malloc() et free(), où free() n'a pas pris d'argument size. L'implémentation était une liste circulaire, chaque bloc était précédé d'un mot qui contenait un pointeur vers le bloc suivant et un bit "occupé" (alloué). Ainsi, malloc()/free() n'a même pas gardé trace d'une taille explicite.

14
ninjalj

C n'est peut-être pas aussi "abstrait" que C++, mais il est toujours destiné à être une abstraction sur Assembly. À cette fin, les détails de niveau le plus bas sont retirés de l'équation. Cela vous évite d'avoir à vous occuper de l'alignement et du remplissage, pour la plupart, ce qui rendrait tous vos programmes C non portables.

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

14

Pourquoi free en C ne prend pas le nombre d'octets à libérer?

Parce qu'il n'en a pas besoin. Les informations sont déjà disponibles dans la gestion interne effectuée par malloc/free.

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

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

    (cela compliquerait pratiquement tout le code client reposant sur la mémoire dynamique et ajouterait une redondance totalement inutile à votre application). Garder une trace de l'allocation des pointeurs est déjà un problème difficile. Garder une trace des allocations de mémoire avec les 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
    

    Ne serait-ce pas gratuit (provoquer une fuite de mémoire?)? Ignorer le deuxième paramètre? Arrêter l'application en appelant exit? L'implémentation ajouterait 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, voir mon dernier point, ci-dessous - "implémenter la solution au niveau de l'application").

Je veux plutôt savoir pourquoi la gratuité a été créée de cette façon en premier lieu.

Parce que c'est la façon "appropriée" de le faire. Une API doit exiger les arguments dont elle a besoin pour effectuer son opération, et pas plus .

Il me semble également 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 en mesure de déterminer de quel pool se libérer simplement en regardant les arguments d'entrée, et il y aurait moins d'espace en général.

Les moyens appropriés pour mettre en œuvre cela sont:

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

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

9
utnapistim

Cinq raisons viennent à l'esprit:

  1. C'est pratique. Il supprime toute une charge de surcharge du programmeur et évite une classe d'erreurs extrêmement difficiles à suivre.

  2. Il 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 n'est pas clair?

  3. Les courses de légèreté en orbite sont parfaites pour le rembourrage et l'alignement. La nature de la gestion de la mémoire signifie que la taille réelle allouée est très probablement différente de la taille que vous avez demandée. Cela signifie que si free nécessitait une taille ainsi qu'un emplacement malloc, il faudrait également modifier la taille réelle allouée.

  4. Il n'est pas clair qu'il y ait un avantage réel à passer la taille, de toute façon. Un gestionnaire de mémoire typique dispose de 4 à 16 octets d'en-tête pour chaque bloc de mémoire, ce qui inclut la taille. Cet en-tête de bloc peut être commun pour la mémoire allouée et non allouée et lorsque des morceaux 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 probablement 4 octets par bloc en n'ayant pas de champ de taille distinct dans la mémoire allouée, mais ce champ de taille n'est probablement pas gagné de toute façon puisque l'appelant doit le stocker quelque part. Mais maintenant, ces informations sont dispersées dans la mémoire plutôt que d'être situées de manière prévisible dans le bloc d'en-tête, ce qui est de toute façon moins efficace sur le plan opérationnel.

  5. Même s'il est était plus efficace, il est radicalement peu probable que votre programme passe beaucoup de temps à libérer de la mémoire de toute façon donc l'avantage serait minime.

Par ailleurs, votre idée d'allocateurs séparés pour des éléments de taille différente est facilement implémentée sans ces informations (vous pouvez utiliser l'adresse pour déterminer où l'allocation s'est produite). Cela se fait régulièrement en C++.

Ajouté plus tard

Une autre réponse, assez ridicule, a fait apparaître std :: allocator comme preuve que free pourrait fonctionner de cette façon mais, en fait, cela sert de bon exemple de la raison pour laquelle free ne fonctionne pas de cette façon. Il y a deux différences principales entre ce que malloc/free font et ce que std :: allocator fait. Premièrement, malloc et free sont destinés aux utilisateurs - ils sont conçus pour que les programmeurs généraux puissent travailler - alors que std::allocator est conçu pour fournir une allocation de mémoire spécialisée à la bibliothèque standard. Cela fournit un bel exemple de quand le premier de mes points n'a pas d'importance, ou ne le serait pas. Puisqu'il s'agit d'une bibliothèque, les difficultés de gérer les complexités 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 cela signifie qu'il lui est possible d'utiliser le nombre d'éléments transmis à l'origine pour déterminer la quantité d'espace libre. Pourquoi cela diffère de free lui-même est illustratif. Dans std::allocator les éléments à allouer sont toujours de la même taille, connus, de taille et toujours du même type d'élément, de sorte qu'ils ont toujours le même type d'exigences d'alignement. Cela signifie que l'allocateur peut être spécialisé pour simplement allouer un tableau de ces éléments au début et les distribuer au besoin. Vous ne pouvez pas le faire avec free car il n'y a aucun moyen de garantir que la meilleure taille à renvoyer est la taille demandée, mais il est beaucoup plus efficace de renvoyer parfois des blocs plus grands que ce que l'appelant demande * et ainsi soit l'utilisateur ou le gestionnaire doit suivre la taille exacte réellement accordée. La transmission de ces types de détails d'implémentation à l'utilisateur est un casse-tête inutile qui ne donne 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 pointeur décalé par rapport à cela. Les informations stockées ici incluent généralement des pointeurs vers le bloc libre suivant, par exemple. Supposons que l'en-tête ne fasse que 4 octets de long (ce qui est en fait plus petit que la plupart des bibliothèques réelles), et n'inclut pas la taille, alors imaginez que nous avons un bloc libre de 20 octets lorsque l'utilisateur demande un bloc de 16 octets, naïf Le système retournerait le bloc de 16 octets mais laisserait ensuite un fragment de 4 octets qui ne pourrait jamais, jamais être utilisé perdre du temps chaque fois que malloc est appelé. Si, à la place, le gestionnaire renvoie simplement le bloc de 20 octets, il enregistre ces fragments désordonnés de la création et est capable d'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 en fait 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. Ainsi, tout au plus, exiger que free prenne une taille est soit (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 inutilement l'utilisateur à faire le suivi de la taille réel qui serait facilement gérée par tout gestionnaire de mémoire sensible.

9
Jack Aidley

Je ne poste cela que comme réponse non pas parce que c'est celle que vous espérez, mais parce que je pense que c'est la seule correcte de manière plausible:

Il a probablement été jugé commode à l'origine et n'a pas pu être amélioré par la suite.
Il n'y a probablement aucune raison convaincante pour cela. (Mais je serai heureux de supprimer ceci si montré qu'il est incorrect.)

Il y aurait des avantages si c'était possible: vous pourriez allouer un seul gros morceau de mémoire dont vous connaissiez la taille au préalable, puis libérer un peu à la fois - par opposition à l'allocation et à la libération répétées de petits morceaux de mémoire. Actuellement, des tâches comme celle-ci ne sont pas possibles.


Pour le beaucoup (beaucoup1!) d'entre vous qui pensent que passer la taille est tellement ridicule:

Puis-je vous renvoyer à la décision de conception de C++ pour le std::allocator<T>::deallocate méthode?

void deallocate(pointer p, size_type n);

Tous les objets nT dans la zone pointée par p doivent être détruits avant cela appel.
n doit correspondre à la valeur passée à allocate pour obtenir cette mémoire.

Je pense que vous aurez plutôt "intéressant" le temps d'analyser cette décision de conception.


Pour ce qui est de operator delete, il s'avère que la proposition 2013 N3778 ("C++ Sized Deallocation") vise également à résoudre ce problème.


1Regardez simplement les commentaires sous la question d'origine pour voir combien de personnes ont fait des affirmations hâtives telles que "la taille demandée est complètement inutile pour l'appel de free" pour justifier le manque du paramètre size.

5
Mehrdad

malloc et free vont de pair, chaque "malloc" étant associé à un "free". Ainsi, il est tout à fait logique que le "libre" correspondant à un "malloc" précédent devrait simplement libérer 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 du monde entier avaient besoin que le programmeur garde une trace du montant alloué dans malloc, puis n'oubliez pas de le libérer. Le scénario dont vous parlez devrait vraiment utiliser plusieurs mallocs/frees dans une sorte d'implémentation de la gestion de la mémoire.

2
Marius George

Je ne vois pas comment fonctionnerait un allocateur qui ne suit pas la taille de ses allocations. S'il ne l'a pas fait, comment pourrait-il savoir quelle mémoire est disponible pour répondre à une future demande 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).

1
M.M

Je dirais 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 sujet aux erreurs de programmation.

De plus, realloc aurait besoin de ces informations de comptabilité, qui, je pense, contiennent plus que la taille de l'allocation. c'est-à-dire qu'il permet au mécanisme par lequel il fonctionne d'être défini par l'implémentation.

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

1
Pete

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. La quantité d'octets est gérée 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 retourné par free (). Je vous suggère une manière manuelle de compter le nombre d'octets/positions alloués par un programme en cours d'exécution:

Si vous travaillez sous Linux et que vous souhaitez connaître la quantité d'octets/positions alloués par malloc, vous pouvez créer un programme simple qui utilise malloc une ou n fois et affiche les pointeurs que vous obtenez. De plus, vous devez mettre le programme en veille pendant quelques secondes (suffisamment pour que vous puissiez effectuer les opérations suivantes). Après cela, lancez ce programme, recherchez son PID, écrivez cd/proc/process_PID et tapez simplement "cat maps". La sortie vous montrera, sur une ligne spécifique, à la fois les adresses de mémoire de début et de fin de la région de mémoire de tas (celle dans laquelle vous allouez de la mémoire de manière dinamique). Si vous imprimez les pointeurs vers ces régions de mémoire allouées, vous peut deviner la quantité de mémoire que vous avez allouée.

J'espère que cela aide!

0
user3416290

Pourquoi cela? malloc () et free () sont une gestion de la mémoire intentionnellement très simple primitives, et la gestion de la mémoire de niveau supérieur en C dépend en grande partie du développeur. T

De plus, realloc () le fait déjà - si vous réduisez l'allocation dans realloc (), cela ne déplacera pas les données et le pointeur renvoyé sera le même que l'original.

Il est généralement vrai que la bibliothèque standard entière est composée de primitives simples à partir desquelles vous pouvez créer des fonctions plus complexes pour répondre aux besoins de votre application. Donc, la réponse à toute question de la forme "pourquoi la bibliothèque standard ne fait pas X" est parce qu'elle ne peut pas faire tout ce à quoi un programmeur peut penser (c'est à cela que les programmeurs sont destinés), donc il choisit de faire très peu - construire le vôtre ou utiliser des bibliothèques tierces. Si vous voulez une bibliothèque standard plus étendue - y compris une gestion de la mémoire plus flexible, alors C++ peut être la réponse.

Vous avez marqué la question C++ ainsi que C, et si C++ est ce que vous utilisez, vous ne devriez en aucun cas utiliser malloc/free - à part new/delete, les classes de conteneurs STL gèrent la mémoire automatiquement et d'une manière probable être spécifiquement adapté à la nature des différents conteneurs.

0
Clifford