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

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

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

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

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

12 ответов


один аргумент 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 *). (В настоящее время этот второй вариант выглядит довольно заманчиво, потому что мы знаем, что строки с нулевым завершением являются "самая катастрофическая ошибка дизайна в истории вычислений", но это задним числом говорить. Еще в 70-х годах способность C обрабатывать строки как простое char * фактически считалось определение преимущества перед конкурентами, такими как Pascal и Algol.) Кроме того, это не просто strdup который страдает от этой проблемы-это влияет на каждую системную или пользовательскую функцию, которая выделяет память кучи.

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

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

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_ts вокруг повсюду, что добавило накладные расходы CPU для копирования и означало, что вам приходилось чаще разливать регистры (esp. для дополнительной аргументы функции) и потерянной памяти (так как вложенные вызовы функций часто приводят к нескольким копиям size_t хранится в разных кадрах стека). В вашей новой системе вам все равно придется потратить память, чтобы сохранить size_t, но только один раз, и он никогда не копируется никуда. Это может показаться небольшой эффективностью, но имейте в виду, что мы говорим о высокопроизводительных машинах с 256 КБ ОЗУ.

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


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

здесь в этом нет необходимости, и не совсем понятно в любом случае.

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

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


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

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


на самом деле, в Древнем распределителе памяти ядра Unix, на


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

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

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

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

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

  • что бы переделывали free функция do, в этих случаях?

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

    это не бесплатное (причина a утечка памяти?)? Игнорировать второй параметр? Остановите приложение, вызвав exit? Реализация этого добавит дополнительные точки отказа в вашем приложении для функции, которая вам, вероятно, не нужна (и если вам это нужно, см. мой последний пункт ниже - "реализация решения на уровне приложения").

скорее, я хочу знать, почему free был сделан таким образом в первую очередь.

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

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

правильные способы реализации, которые, являются:

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

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


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

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

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

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

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

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

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

добавлено позднее

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

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

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


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

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

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


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

могу я сослаться на дизайнерское решение C++для std::allocator<T>::deallocate способ?

void deallocate(pointer p, size_type n);

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

Я думаю, у вас будет довольно "интересные" время анализа этого проектного решения.


как operator delete, оказывается, что 2013 N3778 предложение ("размер C++ Освобождение") призвана исправить это.


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


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


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

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

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


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


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

Если вы работаете в Linux, и вы хотите знать количество байтов / позиций malloc выделенный, вы можете сделать простую программу, которая использует malloc один раз или n раз и распечатывает указатели, которые вы получаете. Кроме того, вы должны сделать программу спящей в течение нескольких секунд (достаточно для вас сделать следующее). После этого запустите эту программу, найдите ее PID, напишите cd/proc / process_PID и просто введите "cat maps". Вывод покажет вам в одной конкретной строке как начальные, так и конечные адреса памяти области памяти кучи (той, в которой вы выделяете память динамически).Если вы распечатываете указатели на эти выделяемые области памяти, вы можете догадаться, сколько памяти вы выделили.

надеюсь, что это помогает!


Почему? malloc() и free () намеренно очень простое управление памятью примитивы, и управление памятью более высокого уровня в C в значительной степени зависит от разработчика. T

кроме того, realloc() делает это уже - если вы уменьшите выделение в realloc (), он не будет перемещать данные, а возвращаемый указатель будет таким же, как и исходный.

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

вы отметили вопрос C++, а также C, и если C++ - это то, что вы используете, то вы вряд ли должны использовать malloc/free в любом случае-кроме new/delete, классы контейнеров STL управляют памятью автоматически и таким образом, который, вероятно, будет специально соответствовать природе различных контейнеров.