Почему `free` в C не берет количество байтов, которые нужно освободить?

Просто для того, чтобы быть ясным: я знаю, что malloc и free реализованы в библиотеке C, которая обычно выделяет куски памяти из ОС и делает свое собственное управление для распределения меньшего количества памяти в приложение и отслеживает количество байтов. Этот вопрос не в том, как бесплатно знать, сколько бесплатно .

Скорее, я хочу знать, почему это было сделано во-первых. Будучи языком низкого уровня, я думаю, было бы вполне разумно попросить программиста C отслеживать не только то, что было выделено для памяти, но и сколько (на самом деле, я обычно обнаруживаю, что в конечном итоге отслеживаю количество байтов malloced в любом случае). Мне также приходит в голову, что явное предоставление количества свободных байтов может позволить некоторым оптимизациям производительности, например, распределитель, который имеет отдельные пулы для разных размеров распределения, сможет определить, какой пул освободится, просто просмотрев входные аргументы, и в целом будет меньше пространства накладных расходов.

Итак, короче говоря, почему malloc и free созданы так, что они должны внутренне отслеживать количество выделенных байтов? Это просто историческая катастрофа?

Небольшое редактирование: несколько человек предоставили такие пункты, как «что, если вы освобождаете другую сумму, чем то, что вы выделили». Мой воображаемый API может просто потребовать, чтобы один точно освобождал количество выделенных байтов; освобождение более или менее может быть просто UB или реализация определена. Однако я не хочу препятствовать обсуждению других возможностей.

Один аргумент free(void *) (введенный в Unix V7) имеет еще одно важное преимущество перед более ранним двух аргументом mfree(void *, size_t) котором я не упоминал здесь: один аргумент free значительно упрощает работу любого другого API, который работает с памятью кучи. Например, если требуется free размер блока памяти, тогда strdup каким-то образом должен будет вернуть два значения (указатель + размер) вместо одного (указатель), а C делает многозначные возвращает намного более громоздким, чем однозначные. Вместо char *strdup(char *) нам пришлось бы писать char *strdup(char *, size_t *) или 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 *) . (В настоящее время этот второй вариант выглядит довольно заманчивым, потому что мы знаем, что строки с нулевым завершением являются «самой катастрофической ошибкой дизайна в истории вычислений» , но это задним числом. Еще в 70-х годах способность C обрабатывать строки как простой char * самом деле считалось определяющим преимуществом перед конкурентами, такими как Pascal и Algol .) Кроме того, это не просто strdup который страдает от этой проблемы – он затрагивает каждую системную или пользовательскую функцию, которая выделяет кучную память.

Ранние дизайнеры Unix были очень умными людьми, и есть много причин, почему free лучше, чем mfree поэтому в основном я думаю, что ответ на этот вопрос заключается в том, что они это заметили и соответствующим образом разработали свою систему. Я сомневаюсь, что вы найдете прямой отчет о том, что происходит в их головах, в тот момент, когда они приняли это решение. Но мы можем себе представить.

Представьте, что вы пишете приложения на C, чтобы работать на V6 Unix, с двумя аргументами mfree . До сих пор вы справлялись с этим, но отслеживание этих размеров указателей становится все более и более сложным, поскольку ваши программы становятся более амбициозными и требуют все большего использования переменных, распределенных кучей. Но тогда у вас есть блестящая идея: вместо копирования вокруг этих size_t s все время вы можете просто написать некоторые служебные функции, которые фиксируют размер непосредственно внутри выделенной памяти:

 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); } 

И чем больше кода вы пишете с помощью этих новых функций, тем более удивительным они кажутся. Они не только упрощают запись кода, но и делают ваш код быстрее – две вещи, которые часто не сочетаются друг с другом! Перед тем, как пропустить эти size_t s по всему месту, что добавило накладные расходы процессора для копирования, и это означало, что вам приходилось чаще проливать регистры (особенно для дополнительных аргументов функции) и впустую память (поскольку вложенные вызовы функций часто приводят к тому, что несколько копий size_t хранятся в разных кадрах стека). В вашей новой системе вам все равно придется тратить память на хранение size_t , но только один раз, и она никогда не копируется нигде. Это может показаться небольшой эффективностью, но имейте в виду, что мы говорим о высокопроизводительных машинах с 256 КБ ОЗУ.

Это делает вас счастливыми! Итак, вы делитесь своим трюком с бородатыми мужчинами, которые работают над следующим выпуском Unix, но это не делает их счастливыми, это делает их печальными. Понимаете, они просто собирали новые функции, такие как strdup , и понимают, что люди, использующие ваш трюк, не смогут использовать свои новые функции, потому что их новые функции используют громоздкий указатель + размер API. И тогда это вас тоже огорчает, потому что вы понимаете, что вам придется переписывать хорошую strdup(char *) самостоятельно в каждой strdup(char *) вами программе вместо того, чтобы использовать версию системы.

Но ждать! Это 1977 год, и обратная совместимость не будет изобретена еще 5 лет! И кроме того, никто не серьезно использует эту неясную вещь «Unix» с ее нецветным именем. Первый выпуск K & R теперь находится на пути к издателю, но это не проблема – на первой странице он прямо говорит, что «C не предоставляет операций для непосредственного взаимодействия с составными объектами, такими как символьные строки … нет кучи … “. На этом этапе истории string.h и malloc являются расширениями поставщиков (!). Итак, предлагает Бородатый Человек №1, мы можем изменить их, как нам нравится; почему бы нам просто не объявить ваш хитрый распределитель официальным распределителем?

Несколько дней спустя, Bearded Man # 2 видит новый API и говорит, эй, подождите, это лучше, чем раньше, но он по-прежнему расходует целое слово на выделение, сохраняя размер. Он рассматривает это как следующее, чтобы богохульствовать. Все остальные смотрят на него так, как будто он сумасшедший, потому что что еще вы можете сделать? В ту ночь он опаздывает и изобретает новый распределитель, который не сохраняет размер вообще, но вместо этого навевает его на лету, выполняя черные черные биты по значению указателя и свопив его, сохраняя новый API на месте. Новый API означает, что никто не замечает коммутатор, но они замечают, что на следующее утро компилятор использует на 10% меньше ОЗУ.

И теперь все счастливы: вы получаете свой более простой для записи и быстрый код, Bearded Man # 1 получает возможность писать приятный простой strdup который люди действительно будут использовать, и Bearded Man # 2 – уверен, что он заработал немного денег – восходит к беспорядку с quines . Отправим его!

Или, по крайней мере, так могло случиться.

«Почему в C не получается освободить количество байтов?»

Потому что в этом нет необходимости, и это все равно не имеет смысла .

Когда вы выделяете что-то, вы хотите сообщить системе, сколько байтов выделять (по понятным причинам).

Однако, когда вы уже выделили свой объект, теперь определяется размер области памяти, в которую вы возвращаетесь. Это неявно. Это один непрерывный блок памяти. Вы не можете освободить его часть (давайте забудем realloc() , это не то, что он делает в любом случае), вы можете только освободить все это. Вы также не можете «освободить X-байты» – либо освободите блок памяти, который вы получили от malloc() либо нет.

И теперь, если вы хотите освободить его, вы можете просто сказать системе диспетчера памяти: «вот этот указатель, free() блок, на который он указывает». – и менеджер памяти будет знать, как это сделать, либо потому, что он неявно знает размер, либо потому , что ему может даже не понадобиться размер.

Например, большинство типичных реализаций malloc() поддерживают связанный список указателей на свободные и выделенные блоки памяти. Если вы передадите указатель на free() , он будет просто искать этот указатель в «выделенном» списке, отключить соответствующий узел и прикрепить его к «свободному» списку. Он даже не нуждался в размере региона. Эта информация понадобится только в том случае, если она потенциально пытается повторно использовать рассматриваемый блок.

C не может быть «абстрактным», как C ++, но он по-прежнему предназначен для абстракции над сборкой. С этой целью детали нижнего уровня извлекаются из уравнения. Это предотвратит вас, если вам придется заниматься с выравниванием и дополнением, по большей части, что сделает все ваши программы C не переносимыми.

Короче говоря, в этом весь смысл писать абстракцию .

На самом деле, в древнем mfree() памяти ядра Unix, mfree() принял аргумент size . malloc() и mfree() сохраняли два массива (один для основной памяти, другой для обмена), содержащий информацию о свободных адресах и размерах блоков.

До Unix V6 не было распределителя пользовательского пространства (программы просто использовали sbrk() ). В Unix V6 iolib включил распределитель с alloc(size) и free() который не принимал аргумент размера. Каждому блоку памяти предшествовал его размер и указатель на следующий блок. Указатель использовался только на свободных блоках, когда шел по свободному списку, и использовался повторно как блок-память на используемых блоках.

В Unix 32V и Unix V7 это было заменено новой реализацией malloc() и free() , где free() не принимал аргумент size . Реализация была циклическим списком, каждому fragmentу предшествовало слово, содержащее указатель на следующий fragment и бит «занятый» (выделенный). Таким образом, malloc()/free() даже не отслеживал явный размер.

Почему в C не получается освободить количество байтов?

Потому что это не нужно. Информация уже доступна во внутреннем управлении, выполняемом malloc / free.

Вот два соображения (которые могли или не могли повлиять на это решение):

  • Почему вы ожидаете, что функция получит параметр, который ему не нужен?

    (это усложнит практически весь клиентский код, основанный на динамической памяти, и добавит полностью ненужное избыточность в ваше приложение). Отслеживание выделения указателя уже является проблемой. Отслеживание распределения памяти вместе с связанными размерами без необходимости увеличивало бы сложность кода клиента.

  • Что бы изменила free функция в этих случаях?

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

    Не будет ли он свободен (причиной утечки памяти?)? Игнорировать второй параметр? Остановить приложение, вызвав exit? Внедрение этого приведет к добавлению дополнительных точек отказа в вашем приложении, для функции, которая вам, вероятно, не нужна (и если вам это нужно, см. Мой последний пункт, ниже – «внедрение решения на уровне приложения»).

Скорее, я хочу знать, почему это было сделано во-первых.

Потому что это «правильный» способ сделать это. API должен требовать аргументы, необходимые для выполнения его работы, и не более того .

Мне также приходит в голову, что явное предоставление количества свободных байтов может позволить некоторым оптимизациям производительности, например, распределитель, который имеет отдельные пулы для разных размеров распределения, сможет определить, какой пул освободится, просто просмотрев входные аргументы, и в целом будет меньше пространства накладных расходов.

Правильными способами реализации этого являются:

  • (на системном уровне) в рамках реализации malloc – нет ничего, что помешало бы разработчику библиотеки писать malloc для использования различных страtagsй внутри, на основе полученного размера.

  • (на уровне приложения), обернув malloc и бесплатно в своих API-интерфейсах и используя их вместо этого (везде в вашем приложении, которое вам может понадобиться).

На ум приходят пять причин:

  1. Это удобно. Он удаляет всю нагрузку накладных расходов у программиста и избегает classа, который чрезвычайно затрудняет отслеживание ошибок.

  2. Это открывает возможность освобождения части блока. Но поскольку менеджеры памяти обычно хотят иметь информацию о отслеживании, неясно, что это будет означать?

  3. Легкость Расы В Орбитах происходит разметка и выравнивание. Характер управления памятью означает, что выделенный фактический размер, возможно, отличается от размера, который вы просили. Это означает, что они могут требовать размер, а также местоположение malloc необходимо будет изменить, чтобы вернуть выделенный размер.

  4. Не ясно, что в любом случае есть какая-то фактическая выгода от прохождения. Обычный диспетчер памяти имеет 4-16 байт заголовка для каждого блока памяти, который включает в себя размер. Этот заголовок блока может быть общим для выделенной и нераспределенной памяти, и когда смежные куски освобождаются, они могут быть свернуты вместе. Если вы создадите память вызывающего абонента свободной памяти, вы можете освободить, возможно, 4 байта на кусок, не имея отдельного поля размера в выделенной памяти, но это поле размера, вероятно, так и не получено, поскольку вызывающему нужно его где-то хранить. Но теперь эта информация разбросана по памяти, а не предсказуемо расположена в блоке заголовка, которая, по всей вероятности, будет менее эффективной.

  5. Даже если это было бы более эффективно, вряд ли ваша программа тратит много времени на освобождение памяти, так что преимущество будет крошечным.

Кстати, ваша идея об отдельных распределителях для разных элементов размера легко реализуется без этой информации (вы можете использовать адрес, чтобы определить, где произошло распределение). Это обычно выполняется на C ++.

Добавлено позже

Другой ответ, довольно смешно, привел в действие std :: allocator как доказательство того, что free может работать таким образом, но на самом деле он служит хорошим примером того, почему free не работает таким образом. Существует два основных различия между тем, что malloc / free делает и что делает std :: allocator. Во-первых, malloc и free ориентированы на пользователей – они предназначены для обычных программистов для работы, тогда как std::allocator предназначен для предоставления специализированной памяти в стандартную библиотеку. Это дает хороший пример того, когда первый из моих пунктов не имеет значения или не имеет значения. Поскольку это библиотека, трудности с обработкой сложностей отслеживания размеров скрыты от пользователя в любом случае.

Во-вторых, std :: allocator всегда работает с одним и тем же элементом размера, это означает, что он может использовать исходно прошедшее количество элементов, чтобы определить, сколько свободного. Почему это отличается от самого free , является иллюстративным. В std::allocator элементы, которые должны быть выделены, всегда одинаковы, известны, имеют размер и всегда один и тот же элемент, поэтому они всегда имеют одинаковые требования к выравниванию. Это означает, что распределитель может быть специализированным, чтобы просто выделить массив этих элементов в начале и использовать их по мере необходимости. Вы не могли бы сделать это free потому что нет возможности гарантировать, что лучший размер для возврата – это требуемый размер, вместо этого гораздо эффективнее иногда возвращать большие блоки, чем запрашивающий запрашивает *, и, следовательно, либо пользователь, либо менеджер должен отслеживать точный размер, фактически предоставленный. Передача этих данных о реализации для пользователя – это ненужная головная боль, которая не приносит пользы вызывающему абоненту.

– * Если кому-то все еще трудно понять этот момент, рассмотрите это: обычный распределитель памяти добавляет небольшое количество информации отслеживания в начало блока памяти, а затем возвращает смещение указателя от этого. Информация, хранящаяся здесь, обычно включает указатели на следующий свободный блок, например. Предположим, что заголовок представляет собой всего лишь 4 байта (что на самом деле меньше, чем большинство реальных библиотек) и не включает размер, тогда представьте, что у нас есть 20-байтовый свободный блок, когда пользователь запрашивает 16-байтовый блок, наивный система вернет 16-байтовый блок, но затем оставит fragment в 4 байта, который никогда не может использоваться, тратя время каждый раз, когда вызывается malloc . Если вместо этого менеджер просто возвращает 20-байтовый блок, он сохраняет эти беспорядочные fragmentы от создания и способен более четко распределять доступную память. Но если система должна правильно это сделать, не отслеживая сам размер, мы требуем от пользователя отслеживать – для каждого, отдельного распределения – объем фактически распределенной памяти, если он должен передать его бесплатно. Тот же аргумент применяется к заполнению для типов / распределений, которые не соответствуют требуемым границам. Таким образом, самое большее, требуя free размера, либо (а) полностью бесполезно, поскольку распределитель памяти не может полагаться на пройденный размер, чтобы соответствовать фактически распределенному размеру, или (б) бессмысленно требует от пользователя выполнения работы, отслеживающей реальную который будет легко обрабатываться любым разумным менеджером памяти.

Я только отправляю это как ответ не потому, что это тот, на который вы надеетесь, а потому, что я считаю, что это единственно правдоподобно:

Вероятно, это было сочтено удобным вначале, и после этого его нельзя было улучшить.
Вероятно, для этого нет убедительной причины. (Но я с радостью удалю это, если будет показано, что оно неверно.)

Было бы полезно, если бы это было возможно: вы могли бы выделить одну большую часть памяти, размер которой вы знали заранее, а затем немного освобождали за раз – в отличие от многократного выделения и освобождения небольших fragmentов памяти. В настоящее время такие задачи невозможны.


Многим (многим!!) Из вас, кто думает, что прохождение размера настолько смехотворно:

Могу ли я передать вам решение C ++ для метода std::allocator::deallocate ?

 void deallocate(pointer p, size_type n); 

Все n T объектов в области, на которые указывает p должны быть уничтожены до этого вызова.
n должно соответствовать значению, переданному для allocate для получения этой памяти.

Я думаю, у вас будет довольно «интересное» время, анализирующее это дизайнерское решение.


Что касается operator delete , то выясняется, что предложение N3778 2013 года («C ++ Sized Deallocation») также должно исправить это.


1 Просто взгляните на комментарии по оригинальному вопросу, чтобы узнать, сколько людей поспешили утверждать такие, как «заданный размер абсолютно бесполезен для free вызова», чтобы оправдать отсутствие параметра size .

malloc и бесплатно идут arm об руку, причем каждый «malloc» подкрепляется одним «бесплатным». Таким образом, общий смысл заключается в том, что «свободный» сопоставление предыдущего «malloc» должен просто высвобождать объем памяти, выделенный этим malloc – это основной вариант использования, который имеет смысл в 99% случаев. Представьте себе все ошибки памяти, если все использования malloc / free всеми программистами по всему миру когда-либо понадобится программисту, чтобы отслеживать сумму, выделенную в malloc, а затем не забывайте освобождать ее. Сценарий, о котором вы говорите, действительно должен использовать несколько mallocs / frees в какой-то реализации управления памятью.

Я бы предположил, что это потому, что очень удобно не вручную отслеживать информацию о размере таким образом (в некоторых случаях), а также менее подвержен ошибкам программиста.

Кроме того, realloc понадобится эта бухгалтерская информация, которая, как я полагаю, содержит больше, чем просто размер распределения. т.е. он позволяет определить механизм, с помощью которого он будет реализован.

Вы могли бы написать свой собственный распределитель, который работал несколько так, как вы предлагаете, и часто это делается в c ++ для распределителей пулов аналогичным образом для конкретных случаев (с потенциально большим приростом производительности), хотя это обычно реализуется с точки зрения оператора новый для выделения блоков пула.

Я не вижу, как будет работать распределитель, который не отслеживает размер его распределений. Если он этого не сделал, как бы он знал, какая память доступна для удовлетворения будущего запроса malloc ? Он должен, по крайней мере, хранить некоторую структуру данных, содержащую адреса и длины, чтобы указать, где находятся доступные блоки памяти. (И, конечно, сохранение списка свободных пространств эквивалентно сохранению списка выделенных пространств).

Ну, единственное, что вам нужно, это указатель, который вы будете использовать, чтобы освободить память, которую вы ранее выделили. Объем байтов – это что-то, управляемое операционной системой, поэтому вам не нужно беспокоиться об этом. Нет необходимости получать количество байтов, переданных функцией free (). Я предлагаю вам ручной способ подсчета количества байтов / позиций, выделенных текущей программой:

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.

Надеюсь, поможет!

Почему это должно быть? 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.