почему std :: allocator :: deallocate требует размера?

std::allocator - это абстракция над базовой моделью памяти, которая включает в себя функции вызова new и delete. delete размер не нужен, но deallocate () требует этого.

void deallocate (T * p, std :: size_t n);
«Аргумент n должен быть равен первому аргументу вызова allocate (), который изначально произвел p; в противном случае поведение не определено ".

Почему?

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


person Trevor Hickey    schedule 04.08.2016    source источник
comment
Наблюдается тенденция к явному предоставлению размера, поскольку это приводит к лучшей оптимизации и ускорению кода кучи. В большинстве случаев компилятор знает об этом, когда вызывается удаление. Я помню это из некоторых разговоров на Going Native или Boostcon об изменениях в распределителе.   -  person JDługosz    schedule 04.08.2016
comment
@ JDługosz Компилятор этого не знает, реализация free библиотеки C знает, а реализация delete [] библиотеки C ++ тоже делает это независимо.   -  person Kuba hasn't forgotten Monica    schedule 04.08.2016
comment
@KubaOber См. n3778. «Компилятор должен вызывать версию с указанным размером, а не с версией без размера, если доступна версия с указанным размером». следовательно, компилятор знает это, и, как я уже сказал, он экономит работу диспетчеру памяти, чтобы найти его на основе указателя. Распределитель, как и operator delete, следует этому новому принципу. Найдите презентацию, если вы этому не верите, или для подробного объяснения причин.   -  person JDługosz    schedule 05.08.2016
comment
Все, что знает компилятор, - это размер удаляемого экземпляра. Он будет работать, если он того же размера, что и тип, изначально выделенный в данном месте. Если тип изменился, например из-за деструктора на месте и нового размещения, удаление по размеру приведет к неопределенному поведению: (Конечно, это не совсем обычный код, но предпочтение по размеру удаления заставляет вашу руку и заставляет перераспределять каждый раз при изменении типа объекта. .. Я не уверен, нравится ли мне это. Я бы хотел увидеть тесты распределителя, которые демонстрируют преимущества этого. У меня есть код, который работает быстрее за счет изменения типа на месте.   -  person Kuba hasn't forgotten Monica    schedule 05.08.2016
comment
Пользователи распределителей знают размер, но я бы не стал поручать компилятору знать размер. Компилятор знает размер удаленного типа и предполагает, что он совпадает с размером первоначально выделенного типа. Это предположение не обязательно должно выполняться, поэтому кажется, что оно вводит новое неопределенное поведение в стандарт, я думаю ... Или теперь мы должны обратить внимание на поддержание этого инварианта в нашем коде.   -  person Kuba hasn't forgotten Monica    schedule 05.08.2016


Ответы (5)


Дизайн std::allocator API - Allocator концепции - призван облегчить работу по потенциальной замене .

std::allocator - это абстракция от базовой модели памяти

Этого не должно быть! В общем, распределителю не нужно использовать C malloc и free, а также delete или не-на месте new. Да, обычно это используется по умолчанию, но механизм распределения - это не просто абстракция над моделью памяти C. Чтобы отличаться от других, часто и заключается вся цель настраиваемого распределителя памяти. Помните, что распределители могут быть заменены: конкретному std::allocator может не потребоваться размер для освобождения, но любые замены, скорее всего, потребуются.

Соответствующая реализация std::allocator может свободно утверждать, что вы действительно передаете правильный n deallocate, и в противном случае зависеть от правильности размера.

Бывает, что malloc и free хранят размер блока в своих структурах данных. Но в целом распределитель может этого не делать, и требовать от него этого - преждевременная пессимизация. Предположим, у вас есть собственный распределитель пула, и вы выделяете блоки по int. В типичной 64-битной системе на хранение 64-битного size_t вместе с 32-битным int было бы 200% накладных расходов. У пользователя распределителя гораздо больше возможностей либо сохранить размер вместе с распределением, либо определить размер более дешевым способом.

Хорошие реализации malloc не хранят размер выделения для каждого небольшого выделения; они и могут получить размер блока из самого указателя, например. путем получения указателя блока из указателя блока, а затем проверки заголовка блока на предмет размера блока. Это, конечно, мелочь. Вы можете получить нижнюю границу размера, используя API-интерфейсы для конкретной платформы, такие как _ 17_ в OS X, _msize в Windows, malloc_usable_size в Linux.

person Kuba hasn't forgotten Monica    schedule 04.08.2016

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

Если запросы на выделение 256 и 768 байтов удовлетворяются с использованием смежной области пула, состояние диспетчера памяти будет идентично тому, что было бы, если бы два запроса на 512 байтов были удовлетворены с использованием той же самой области. Если бы диспетчеру памяти передали указатель на первый блок и попросили освободить его, у него не было бы возможности узнать, был ли первый запрос для 256 байтов, 512 байтов или любого другого числа, и, следовательно, не было бы возможности узнать сколько памяти следует добавить обратно в пул.

Реализация malloc и free в такой системе потребует, чтобы она сохраняла длину каждого блока в начале своей области хранения и возвращала указатель на следующий соответствующим образом выровненный адрес, который будет доступен после этой длины. Реализация, безусловно, может это сделать, но это добавит 4-8 байтов служебных данных к каждому распределению. Если вызывающий может сообщить подпрограмме освобождения памяти, сколько памяти нужно добавить обратно в пул памяти, такие накладные расходы могут быть устранены.

person supercat    schedule 04.08.2016

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

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

Теперь, учитывая, что мы говорим о C ++, в который уже встроена куча отличных контейнерных классов, я искренне рекомендую вам избегать «катания своего собственного», если вы можете этого избежать. Просто найдите способ использовать один из изящных контейнерных классов, которые язык и стандартная библиотека уже предоставляют.

В противном случае не забудьте упаковать то, что вы здесь создаете, как собственный контейнерный класс. Убедитесь, что логика, которая имеет дело с распределителем и освободителем, встречается в вашей программе только один раз. (А именно, внутри этого класса.) Щедро добавьте в него логику, специально разработанную для обнаружения ошибок. (Например, контрольное значение, которое вставляется в объект при его выделении, и которое должно быть найдено, когда объект освобождается, и которое стирается непосредственно перед этим. Явные проверки сохраненного значения размера, чтобы сделать уверен, что это имеет смысл. И так далее.)

person Mike Robinson    schedule 04.08.2016

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

Стандартная библиотека старается быть как можно универсальной. Некоторым распределителям необходимо отслеживать размер, другим - нет, но все распределители должны соответствовать интерфейсу.

person user6678809    schedule 04.08.2016
comment
Это неправильно: да, пользователь любого распределителя должен отслеживать размер, поскольку распределители предназначены для замены. std::allocator является реализацией концепции Allocator. Все такие реализации требуют, чтобы в deallocate был передан правильный n! Они могут игнорировать его, но пользователь, тем не менее, должен предоставить его, поскольку использование n является деталью реализации. Конечно, пользователь может сохранить размер в самом выделении, если он того пожелает. - person Kuba hasn't forgotten Monica; 04.08.2016
comment
@KubaOber Вы, кажется, неправильно поняли мой ответ. Вам не нужно отслеживать n, если это фиксированный размер. Это деталь реализации. Стандарт не может предсказать, как будут реализованы все распределители. Так что интерфейс жесткий. - person user6678809; 04.08.2016
comment
Неважно, как вы угадываете n, пока вы передаете правильное значение, вы делаете это правильно. Конечно, вам не нужно явное хранилище для n, но вы тупите, если говорите это, поскольку вам не нужно отслеживать n. Да, вам нужно отслеживать это, но не обязательно с помощью переменной или любого другого хранилища времени выполнения. Типичный стандартный распределитель, использующий malloc, не предполагает какого-либо размера для всех распределений: вы передаете правильный n allocate. Если он использует free вместо delete[], ему потребуется правильный n для вызова деструкторов! - person Kuba hasn't forgotten Monica; 04.08.2016
comment
Извините, конечно, deallocate не вызывает деструкторы. - person Kuba hasn't forgotten Monica; 04.08.2016

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

person SergeyA    schedule 04.08.2016