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

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

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

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

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

Ответ 1

Один аргумент 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_t все время, вы можете просто написать некоторые служебные функции, которые фиксируют размер непосредственно внутри выделенной памяти:

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

Это делает вас счастливыми! Итак, вы делитесь своим трюком с бородатыми мужчинами, которые работают над следующим выпуском Unix, но это не делает их счастливыми, это делает их печальными. Понимаете, они просто собирались добавить новые функции, такие как strdup, и они понимают, что люди, использующие ваш трюк, не смогут использовать свои новые функции, потому что их новые функции все используют громоздкие указатель + размер API. И тогда это вас тоже огорчает, потому что вы понимаете, что вам придется переписывать хорошую функцию 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. Отправляй это!

Или, по крайней мере, как это могло произойти.

Ответ 2

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

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

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

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

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

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

Ответ 3

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

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

Ответ 4

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

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

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

Ответ 5

Пять причин spring:

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

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

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

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

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

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

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

Другой ответ, довольно смешно, поднял 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 принимать размер, либо (a) полностью бесполезен, поскольку распределитель памяти не может полагаться на переданный размер, чтобы соответствовать фактически распределенному размеру, или (б) бессмысленно требует от пользователя выполнения отслеживания работы реальный размер, который легко обрабатывается любым разумным менеджером памяти.

Ответ 6

Почему 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 для использования различных стратегий внутри, на основе полученного размера.

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

Ответ 7

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

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

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


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

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

void deallocate(pointer p, size_type n);

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

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


Что касается operator delete, оказывается, что предложение 2012 N3778 ( "С++ Sized Deallocation" ) предназначено для исправления этого, тоже.


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

Ответ 8

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

Ответ 9

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

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

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

Ответ 10

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

Ответ 11

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

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

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

Ответ 12

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

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

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

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