it-swarm.com.ru

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

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

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

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

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

82
jaymmer

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

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

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

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

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

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

Несколько дней спустя, Бородатый Человек № 2 видит новый API и говорит: эй, подождите, это лучше, чем раньше, но он по-прежнему тратит целое Word на выделение, сохраняя размер. Он рассматривает это как следующую вещь к богохульству. Все остальные смотрят на него как на сумасшедшего, потому что еще ты можешь сделать? Той ночью он опаздывает и изобретает новый распределитель, который вообще не сохраняет размер, а вместо этого выводит его на лету, выполняя сдвиги чёрной магии над значением указателя, и заменяет его, сохраняя новый API на месте. Новый API означает, что никто не замечает переключение, но они замечают, что на следующее утро компилятор использует на 10% меньше оперативной памяти.

И теперь все счастливы: вы получаете свой более простой для написания и более быстрый код, Бородатый мужчина # 1 получает возможность написать хороший простой strdup, который люди фактически будут использовать, и Бородатый мужчина № 2 - уверенный, что он заработал свою долю на некоторое время - возвращается к бездельничает с квинами . Отправим его!

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

94
Nathaniel J. Smith

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

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

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

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

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

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

31
The Paramagnetic Croissant

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

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

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

14
ninjalj

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

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

14
Lightness Races in Orbit

Почему 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
    

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

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

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

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

Надлежащие способы реализовать это:

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

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

9
utnapistim

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

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

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

  3. Lightness Races In Orbit - это то, что касается отступов и выравнивания. Природа управления памятью означает, что выделенный размер фактический вполне может отличаться от размера, который вы просили. Это означает, что для free требовался размер, а также местоположение 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-ти байтовый блок, но затем оставит 4-х байтовый фрагмент, который никогда не сможет быть использован, тратя время каждый раз, когда вызывается malloc. Если вместо этого менеджер просто возвращает 20-байтовый блок, то он предотвращает накопление этих беспорядочных фрагментов и может более аккуратно распределять доступную память. Но если система должна делать это правильно, не отслеживая сам размер, мы требуем, чтобы пользователь отслеживал - для каждого отдельного выделения - объем памяти фактически выделенный, если он должен вернуть его бесплатно , Тот же аргумент применяется к заполнению для типов/распределений, которые не соответствуют желаемым границам. Таким образом, самое большее, требование free для получения размера либо (а) совершенно бесполезно, так как распределитель памяти не может полагаться на переданный размер, чтобы соответствовать фактически выделенному размеру, либо (б) бессмысленно требует, чтобы пользователь отслеживал - реальный размер, который будет легко обработан любым разумным менеджером памяти.

9
Jack Aidley

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

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

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


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

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

void deallocate(pointer p, size_type n);

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

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


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


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

5
Mehrdad

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

2
Marius George

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

1
M.M

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

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

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

1
Pete

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

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

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

0
user3416290

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

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

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

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

0
Clifford