Страницы

Поиск по вопросам

четверг, 28 ноября 2019 г.

Избыточность оператора delete[] в С++

#c++ #выделение_памяти


Тут я как всегда не вовремя задумался вот над каким вопросом. При выделении памяти
из кучи очевидно, что в куче должна сохраняться информация о размере запрошенной области
памяти и о количестве запрошенных элементов. Зачем же тогда существуют отдельно операторы
delete и delete[]? Ведь независимо от того, запросили мы вектор или один элемент, в
куче есть информация и о размере запрошенной области, и о количестве запрошенных элементов.
Не может быть, чтобы при запросе одного элемента в куче не сохранялась информация о
том, что запрошен один элемент. А если это так, то оператор delete вполне может разобраться
(по служебной информации, содержащейся в куче) был ли запрошен массив или был запрошен
один элемент. И, соответственно, вернуть в кучу память или одного элемента, или вектора.
Получается, что оператор delete[] избыточен.

UPD1:


  В нынешнем подходе С++ поступает более экономично: хранит свою
  дополнительную информацию только в массивах объектов с нетривиальным
  деструктором.


То есть Вы хотите сказать, что у кучи есть несколько форматов? Один формат для запрошенного
одного элемента, другой формат для запрошенного вектора, третий формат для запрошенного
одного элемента с нетривиальным аллокатором/деструктором, четвертый формат для запрошенного
вектора с нетривиальным аллокатором/деструктором? Ну, это дело конечно разработчиков
компилятора и кучи, как там они видят свою задачу чтобы сделать ее максимально эффективной.
Но на первый взгляд иметь много форматов кучи это не так чтобы однозначно было эффективнее,
чем хранить всю информацию и в рантайме разбираться, что же именно там сейчас лежит.
Тем более, что разбираться в рантайме и при подходе со многими форматами кучи все равно
придется.

UPD2:

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

UPD3:

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

Итак, есть куча и есть операторы new и new[]. Оба оператора обязаны занести в служебную
информацию кучи данные о размере одного объекта и о КОЛИЧЕСТВЕ ОБЪЕКТОВ в запросе.
Соответственно, оператор возврата памяти delete нужен только один, так как по служебной
информации рантайм может и должен разобраться сколько именно объектов было запрошено.
Соответственно, оператор delete[] избыточен.

Теперь рассмотрим нестандартные (пользовательские) аллокаторы. Совершенно так же
пользовательские new и new[] обязаны занести в служебную информацию кучи данные о размере
одного объекта и о КОЛИЧЕСТВЕ ОБЪЕКТОВ в запросе. Дополнительно пользовательские new
и new[] обязаны занести в служебную информацию кучи указатель на пользовательский деструктор.
Опять же в этом случае оператор возврата памяти delete нужен только один, так как по
служебной информации рантайм может и должен разобраться сколько именно объектов было
запрошено. Соответственно, оператор delete[] избыточен.
    


Ответы

Ответ 1



Во-первых, даже если в куче и сохраняется информация о размере запрошенного блока в байтах, способ хранения этой информации может быть известен delete только в том случае, если используется "штатный" аллокатор. Но процесс выделения "сырой" памяти в С++ является перегружаемым пользователем. Как только выделение памяти перешло на пользовательский аллокатор, delete уже не может определить размер блока. Во-вторых, даже если размер блока в байтах известен, по этому размеру все равно нельзя однозначно восстановить точное количество элементов в массиве, чтобы вызвать точное количество деструкторов. Размер блока может превышать точное значение, требуемое для хранения элементов. В-третьих, не ясно, о каком "количестве запрошенных элементов" вы говорите. Количество запрошенных элементов сохраняет именно new [] и вычитывает именно delete []. Для того, собственно, delete [] и сделан отдельным от delete. Смотрите детали здесь: Откуда C/C++ знает сколько надо освободить памяти, если не знает размер массива? Теоретически можно сделать "умный delete", который сам всегда во всем разбирается. Но это приведет к безусловной необходимости хранить дополнительную информацию во всех блоках памяти. В нынешнем подходе С++ поступает более экономично: хранит свою дополнительную информацию только в массивах объектов с нетривиальным деструктором. Фактически почти такой "умный delete" у вас уже есть. Никто вам не запрещает везде просто безусловно пользоваться new[]/delete[] и забыть про существование new/delete. То есть одиночные объекты просто выделять как массивы размера 1. Но это будет несколько более расточительно (и не поддерживает полиморфного удаления). Отвечая на ваш UPD1: В типичной реализации у блока в С++ куче фактически три формата: для одиночного объекта (new), для массива с тривиальными деструкторами (new[]) и для массива с нетривиальными деструкторами (new[]). При этом первые два формата можно было бы считать совпадающими с точки зрения внутренней структуры, т.к. это просто "блоки памяти". Но тут вмешивается тот факт, что механизмы выделения/освобождения "сырой" памяти в С++ являются перегружаемыми пользователем: независимо для new/delete и для new[]/delete[]. Поэтому это - отдельные форматы. Ответ на ваш UPD3: Я не знаю, с чего вы взяли, что "Оба оператора обязаны занести в служебную информацию кучи данные о размере одного объекта и о КОЛИЧЕСТВЕ ОБЪЕКТОВ в запросе". Это совершенно не так. Еще раз: просто new такой информации НЕ сохраняет. И new[] для типов с тривиальным деструктором никакой информации о количестве или размере объектов НЕ хранит тоже. Эти форматы просто выделяют память через обычный malloc, освобождают через обычный free и никакой дополнительной внутренней информации в этом блоке памяти не сохраняют. С точки зрения С++ памяти требуется ровно столько, сколько нужно для хранения пользовательских данных. Особняком стоит только new[] для массива с нетривиальными деструкторами. Только он сохраняет в блоке служебную информацию о точном количестве элементов в массиве (и поэтому выделяет несколько больше памяти, чем требуется для пользовательских данных). Информация о размере одного элемента в таком блоке не хранится вообще никогда - это низачем не нужно. Я при этом говорю только о стандартных аллокаторах. Пользовательские аллокаторы тут ни при чем.

Ответ 2



Вы смешиваете в одну кучу интерфейс и детали реализации. И, вероятнее всего, забываете, что С++ используется не только на x86-совмесимых системах. Язык (интерфейс) ничего не знает о куче. Это реализация. New вполне может выделять память, например, из slab-аллокатора, в то время как New[] будет использовать кучу. К тому же на микроконтроллерах есть серьезное ограничение по памяти, и делать одинаковую реализацию New/Delete и New[]/Delete[] просто расточительно, т.к. вторая пара должна знать не только размер участка памяти, но и количество реальных элементов в нем.

Ответ 3



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

Ответ 4



Насколько мне известно, существование new / delete и new[] / delete[] обосновано девизом: не платим за то, что не используем. Такой подход позволяет экономить память и такты на лишних проверках. Безусловно, менеджер памяти хранит информацию о размере каждого блока, однако одного лишь размера блока памяти недостаточно, чтобы корректно обработать удаление массива объектов, вызвав для каждого объекта его деструктор. В C такой проблемы разделения нет, потому что free() просто освобождает память, не вызывая деструкторы. Если же мы попытаемся удалить массив объектов C++, используя только информацию о размере блока памяти, то мы не сможем этого сделать. Нам не хватает информации. Поэтому, new[] заводит для каждого массива объектов минимум два счетчика. Содержимое этих счетчиков зависит от реализации компилятора, но обычно в счетчиках хранятся размер блока памяти и тип объекта. Зная тип объекта - мы имеем информацию о размере объекта и его деструкторе. Только в этом случае информации становится достаточно, чтобы корректно вызвать деструктор для каждого объекта массива, а затем освободить блок памяти. Если бы существовал только delete, который умел бы работать и с одиночными объектами, и с массивами объектов, тогда при работе с одиночными объектами происходил бы перерасход памяти и тактов процессора на лишние проверки. Подтверждением этой теории служит тот факт, что следующий код: while (1) { float *arr = new float[1000]; delete arr; arr = new float; delete[] arr; } В большинстве ситуацией не приводит к падениям или утечкам. Да, это UB, но я видел множество случаев такого кода, который был написан умышленно и работал годами. Поскольку float не имеет деструктора, то и проблем при перепутывании delete и delete[] чаще всего не возникает. Конечно, внутри все это устроено намного сложнее, но общая суть происходящего примерно такая.

Комментариев нет:

Отправить комментарий