Чтобы быть ясным: я знаю, что malloc
и free
они реализованы в библиотеке C, которая обычно выделяет фрагменты памяти из ОС и выполняет собственное управление для распределения меньших объемов памяти для приложения и отслеживает количество выделенных байтов. . Этот вопрос не в том, как бесплатно узнать, сколько бесплатно .
Скорее, я хочу знать, почему вообще free
было сделано именно так. Поскольку я низкоуровневый язык, я думаю, было бы вполне разумно попросить программиста на C отслеживать не только то, какая память была выделена, но и сколько (на самом деле, я обычно обнаруживаю, что в конечном итоге отслеживаю количество байтов все равно malloced). Мне также приходит в голову, что явное указание количества байтов free
может позволить некоторую оптимизацию производительности, например, распределитель, у которого есть отдельные пулы для разных размеров распределения, сможет определить, от какого пула освободить, просто взглянув на входные аргументы, и в целом будет меньше накладных расходов.
Итак, вкратце, почему были malloc
free
созданы и созданы такие, что от них требуется внутреннее отслеживать количество выделенных байтов? Это просто историческая случайность?
Небольшое изменение: несколько человек предоставили такие пункты, как «что, если вы освободите сумму, отличную от той, которую вы выделили». В моем воображаемом API может просто потребоваться освободить ровно столько выделенных байтов; освобождение более или менее может быть просто определено UB или реализацией. Однако я не хочу препятствовать обсуждению других возможностей.
источник
malloc
не знает размер возвращенного блока .malloc
часто возвращает блок, превышающий запрошенный. В лучшем случае программист мог передать размер, запрошенный вmalloc()
вызове, что совершенно не помогло бы разработчикуfree()
.Ответы:
Один аргумент
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-х годах способность 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
повсюду, что увеличивало нагрузку на ЦП для копирования и означало, что вам приходилось чаще проливать регистры (особенно для дополнительных аргументов функции) и тратить впустую память (поскольку вызовы вложенных функций часто приводят к в нескольких копияхsize_t
, хранящихся в разных кадрах стека). В вашей новой системе вам все равно придется тратить память для храненияsize_t
, но только один раз и никогда никуда не копируется. Это может показаться небольшой эффективностью, но имейте в виду, что мы говорим о высокопроизводительных машинах с 256 КБ ОЗУ.Это делает тебя счастливым! Итак, вы делитесь своим крутым трюком с бородатыми мужчинами, которые работают над следующей версией Unix, но это не делает их счастливыми, а огорчает. Видите ли, они только что добавляли кучу новых служебных функций, например
strdup
, и понимают, что люди, использующие ваш крутой трюк, не смогут использовать свои новые функции, потому что все их новые функции используют громоздкий указатель + размер API. И тогда вас это тоже огорчает, потому что вы понимаете, что вам придется самостоятельно переписывать хорошуюstrdup(char *)
функцию в каждой программе, которую вы пишете, вместо того, чтобы использовать системную версию.Но ждать! Это 1977 год, а обратной совместимости не придумают еще лет 5! Кроме того, никто из серьезных людей на самом деле не использует эту непонятную «Unix» штуку с ее нечетким названием. Первое издание K&R сейчас на пути к издателю, но это не проблема - прямо на первой странице написано, что «C не предоставляет операций для работы непосредственно с составными объектами, такими как символьные строки ... нет кучи ... ". На данный момент в истории
string.h
иmalloc
стоят расширения поставщиков (!). Итак, предлагает Бородатый Мужчина №1, мы можем изменить их, как захотим; почему бы нам просто не объявить ваш сложный аллокатор официальным распределителем?Через несколько дней Бородатый Человек №2 видит новый API и говорит: «Эй, подожди, это лучше, чем раньше, но он по-прежнему тратит целое слово на выделение памяти, сохраняя размер. Он считает это богохульством. Все смотрят на него, как на сумасшедшего, потому что что еще ты можешь сделать? В ту ночь он задерживается допоздна и изобретает новый распределитель, который вообще не хранит размер, а вместо этого определяет его на лету, выполняя сдвиги битов черной магии для значения указателя и меняя его местами, сохраняя при этом новый API. Новый API означает, что никто не замечает переключение, но они замечают, что на следующее утро компилятор использует на 10% меньше оперативной памяти.
И теперь все счастливы: вы получаете свой более простой в написании и более быстрый код, Бородатый Человек №1 может написать красивый простой,
strdup
который люди действительно будут использовать, а Бородатый Человек №2 - уверен, что он немного заработал на себе - - возвращается к возням с лебедой . Отправим его!По крайней мере, так могло случиться.
источник
Потому что в этом нет необходимости, да и смысла в этом нет .
Когда вы что-то выделяете, вы хотите сообщить системе, сколько байтов нужно выделить (по очевидным причинам).
Однако, когда вы уже выделили свой объект, теперь определяется размер возвращаемой области памяти. Это неявно. Это один непрерывный блок памяти. Вы не можете освободить его часть (давайте забудем
realloc()
, это не то, что он делает), вы можете только освободить его целиком. Вы также не можете «освободить X байтов» - вы либо освобождаете полученный блок памяти,malloc()
либо нет.А теперь, если вы хотите освободить его, вы можете просто сказать системе диспетчера памяти: «вот этот указатель,
free()
блок , на который он указывает». - и диспетчер памяти будет знать, как это сделать, либо потому, что он неявно знает размер, либо потому , что он может даже не нуждаться в размере.Например, наиболее типичные реализации
malloc()
поддерживают связанный список указателей на свободные и выделенные блоки памяти. Если вы передадите указатель наfree()
, он просто будет искать этот указатель в «выделенном» списке, отсоединять соответствующий узел и присоединять его к «свободному» списку. Ему даже не нужен размер региона. Эта информация понадобится ему только тогда, когда он потенциально попытается повторно использовать рассматриваемый блок.источник
C может быть не таким «абстрактным», как C ++, но он все равно должен быть абстракцией над сборкой. С этой целью из уравнения исключаются детали самого низкого уровня. Это избавляет вас от необходимости возиться с выравниванием и заполнением, по большей части, что сделало бы все ваши программы C непереносимыми.
Короче говоря, в этом весь смысл написания абстракции .
источник
malloc
N байтов, и вместо этого он вернул указатель на начало всей страницы (из-за выравнивания, заполнения или других ограничений у пользователя не было бы возможности отслеживать это - заставляя их сделать это было бы контрпродуктивно.malloc
может просто всегда возвращать выровненный указатель, даже не сохраняя размер выделения.free
затем можно округлить до соответствующего выравнивания, чтобы все было освобождено должным образом. Я не понимаю, в чем проблема.size
параметра вfree
открывает еще один источник ошибок.Фактически, в древнем распределителе памяти ядра Unix
mfree()
былsize
аргумент.malloc()
иmfree()
хранил два массива (один для основной памяти, другой для подкачки), которые содержали информацию об адресах и размерах свободных блоков.До Unix V6 не было распределителя пользовательского пространства (программы просто использовали
sbrk()
). В Unix V6, iolib включал аллокатор сalloc(size)
иfree()
вызовом , который не принимал аргумент размера. Каждому блоку памяти предшествовал его размер и указатель на следующий блок. Указатель использовался только для свободных блоков при просмотре списка свободных блоков и повторно использовался в качестве блочной памяти для используемых блоков.В Unix 32V и в Unix V7, это было заменено новым
malloc()
иfree()
реализацией, гдеfree()
не принятьsize
аргумент. Реализация представляла собой круговой список, каждому фрагменту предшествовало слово, содержащее указатель на следующий фрагмент, и бит «занято» (выделено). Такmalloc()/free()
что даже не отслеживал явный размер.источник
Потому что в этом нет необходимости. Информация уже доступна во внутреннем управлении, выполняемом 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 и free в свои собственные API-интерфейсы и используя их вместо них (везде в вашем приложении, которые могут вам понадобиться).
источник
На ум приходят пять причин:
Это удобно. Это избавляет программиста от накладных расходов и позволяет избежать класса чрезвычайно сложных для отслеживания ошибок.
Это открывает возможность освобождения части блока. Но поскольку менеджеры памяти обычно хотят иметь информацию об отслеживании, не ясно, что это будет значить?
Lightness Races In Orbit отлично подходит для заполнения и выравнивания. Природа управления памятью означает, что фактически выделенный размер может отличаться от запрошенного. Это означает, что если
free
бы потребовался размер, а также местоположениеmalloc
, пришлось бы изменить, чтобы вернуть фактический размер.В любом случае неясно, есть ли какая-то реальная польза от увеличения размера. Типичный менеджер памяти имеет 4-16 байтов заголовка для каждого фрагмента памяти, включая размер. Этот заголовок блока может быть общим для выделенной и нераспределенной памяти, и когда соседние блоки освобождаются, они могут быть свернуты вместе. Если вы заставляете вызывающего абонента хранить свободную память, вы можете освободить, вероятно, 4 байта на блок, не имея отдельного поля размера в выделенной памяти, но это поле размера, вероятно, все равно не будет получено, поскольку вызывающему абоненту необходимо где-то его сохранить. Но теперь эта информация разбросана по памяти, а не предсказуемо расположена в блоке заголовка, который в любом случае, вероятно, будет менее эффективен с операционной точки зрения.
Даже если бы это было более эффективно, крайне маловероятно, что ваша программа в любом случае тратит много времени на освобождение памяти, поэтому выгода будет незначительной.
Кстати, ваша идея об отдельных распределителях для элементов разного размера легко реализуется без этой информации (вы можете использовать адрес, чтобы определить, где произошло распределение). Это обычно делается в 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
взять размер либо (а) полностью бесполезно, поскольку распределитель памяти не может полагаться на переданный размер для соответствия фактически выделенному размеру, либо (б) бессмысленно требует, чтобы пользователь выполнял работу, отслеживающую реальный размер, который будет легко обработан любым разумным менеджером памяти.источник
free
вызов должен правильно передать размер, который нужно освободить, он должен это знать.Я публикую это только в качестве ответа не потому, что это тот, на который вы надеетесь, а потому, что я считаю, что это единственный правдоподобный ответ:
Вероятно, изначально это считалось удобным, и впоследствии не могло быть улучшено.
Скорее всего, для этого нет убедительной причины. (Но я с радостью удалю это, если окажется, что это неверно.)
Там будет иметь преимущества , если это было возможно , вы могли бы выделить один большой кусок памяти, размер которого заранее знал, то освободить немного в то время , - в отличие от многократно выделения и освобождения небольших участков памяти. В настоящее время подобные задачи невозможны.
К многочастичным (много 1 !), Кто думает , передавая размер так смешно:
Могу я сослаться на дизайнерское решение C ++ для этого
std::allocator<T>::deallocate
метода?void deallocate(pointer p, size_type n);
Думаю, вам будет довольно «интересно» проанализировать это дизайнерское решение.
Что касается
operator delete
, оказывается, что предложение N3778 2013 года («C ++ Sized Deallocation») предназначено для исправления и этого.1 Просто посмотрите на комментарии под исходным вопросом, чтобы увидеть, сколько людей сделали поспешные утверждения, такие как «запрашиваемый размер совершенно бесполезен для
free
вызова», чтобы оправдать отсутствиеsize
параметра.источник
malloc()
реализации это также устранит необходимость запоминать что-либо о выделенной области, уменьшив накладные расходы на выделение до накладных расходов на выравнивание.malloc()
смогут вести всю бухгалтерию в освобожденных кусках. Есть варианты использования, в которых это было бы большим улучшением. Не рекомендуется выделять большой кусок памяти на несколько небольших, хотя это резко усилит фрагментацию.std::allocator
выделяет только элементы определенного известного размера. Это не универсальный распределитель, сравниваются яблоки с апельсинами.malloc и free идут рука об руку, причем каждому "malloc" соответствует один "free". Таким образом, вполне логично, что «свободное» соответствие предыдущему «malloc» должно просто освобождать объем памяти, выделенный этим malloc - это основной вариант использования, который имеет смысл в 99% случаев. Представьте себе все ошибки памяти, если для всех случаев использования malloc / free всеми программистами по всему миру программист должен отслеживать объем, выделенный в malloc, а затем не забывать освобождать его. Сценарий, о котором вы говорите, действительно должен использовать несколько mallocs / frees в какой-то реализации управления памятью.
источник
gets
,printf
, ручные петли (оффлайновые по одному), неопределенных поведений, формат-строк, неявные преобразования, битовые трюки, и др так далее.Я бы предположил, что это потому, что очень удобно не отслеживать вручную информацию о размере таким образом (в некоторых случаях), а также меньше подвержено ошибкам программиста.
Кроме того, для realloc потребуется эта бухгалтерская информация, которая, как я ожидаю, содержит больше, чем просто размер выделения. т.е. он позволяет определить механизм, с помощью которого он работает.
Вы могли бы написать свой собственный распределитель, который работал бы так, как вы предлагаете, хотя это часто делается в C ++ для распределителей пула подобным образом для конкретных случаев (с потенциально значительным увеличением производительности), хотя обычно это реализуется с помощью оператора новый для распределения блоков пула.
источник
Я не понимаю, как будет работать распределитель, который не отслеживает размер своих распределений. Если бы он этого не сделал, как бы он узнал, какая память доступна для удовлетворения будущего
malloc
запроса? Он должен, по крайней мере, хранить какую-то структуру данных, содержащую адреса и длины, чтобы указать, где находятся доступные блоки памяти. (И, конечно же, сохранение списка свободных пространств эквивалентно хранению списка выделенных пространств).источник
Что ж, единственное, что вам нужно, это указатель, который вы будете использовать для освобождения ранее выделенной памяти. Количество байтов определяется операционной системой, поэтому вам не о чем беспокоиться. Нет необходимости получать количество выделенных байтов, возвращаемое функцией free (). Я предлагаю вам вручную подсчитать количество байтов / позиций, выделенных работающей программой:
Если вы работаете в Linux и хотите знать количество байтов / позиций, выделенных malloc, вы можете создать простую программу, которая использует malloc один или n раз и распечатывает полученные указатели. Кроме того, вы должны перевести программу в спящий режим на несколько секунд (достаточно, чтобы вы могли сделать следующее). После этого запустите эту программу, найдите ее PID, напишите cd / proc / process_PID и просто введите «cat maps». Вывод покажет вам, в одной конкретной строке, и начальный, и конечный адреса памяти для области памяти кучи (той, в которой вы распределяете память динамически). Если вы распечатаете указатели на эти выделяемые области памяти, вы можете угадать, сколько памяти вы выделили.
Надеюсь, поможет!
источник
Зачем это нужно? malloc () и free () намеренно являются очень простыми примитивами управления памятью , а управление памятью более высокого уровня в C в значительной степени зависит от разработчика. Т
Более того, realloc () уже делает это - если вы уменьшите выделение в realloc (), он не будет перемещать данные, а возвращаемый указатель будет таким же, как оригинал.
Обычно для всей стандартной библиотеки справедливо то, что она состоит из простых примитивов, из которых вы можете создавать более сложные функции в соответствии с потребностями вашего приложения. Итак, ответ на любой вопрос в форме «почему стандартная библиотека не выполняет X» заключается в том, что она не может делать все, о чем может подумать программист (для этого и предназначены программисты), поэтому она предпочитает делать очень мало - создавать свою собственную или использовать сторонние библиотеки. Если вам нужна более обширная стандартная библиотека, включая более гибкое управление памятью, то C ++ может быть ответом.
Вы отметили вопрос C ++, а также C, и если C ++ - это то, что вы используете, то вряд ли вам в любом случае следует использовать malloc / free - помимо new / delete, классы контейнеров STL управляют памятью автоматически и, вероятно, чтобы быть особенно подходящим для различных контейнеров.
источник
realloc
это абсолютно разрешено перемещать данные. Согласно стандарту C это совершенно законно реализовыватьrealloc
какmalloc
+memcpy
+free
. И есть веские причины, по которым реализация может захотеть переместить выделение, размер которого был уменьшен, например, чтобы избежать фрагментации памяти.