2015-12-22 1 views
14

С ++ 14 введена "sized" versions of operator delete, т.е.Как использовать операторы размера delete/delete [] и почему они лучше?

void operator delete(void* ptr, std::size_t sz); 

и

void operator delete[](void* ptr, std::size_t sz); 

Чтение через N3536, кажется, что эти операторы были введены для повышения производительности. Я знаю, что типичный распределитель, используемый operator new, «хранит» размер объемной памяти где-то, и так типично operator delete «знает», сколько памяти возвращается в свободный магазин.

Я не уверен, однако, почему «размерные» версии operator delete помогут с точки зрения производительности. Единственное, что может ускорить работу, - это еще одна операция чтения относительно размера блока управления. Это действительно единственное преимущество?

Во-вторых, как я могу справиться с версией массива? AFAIK, размер выделенного массива - это не просто sizeof(type)*number_elements, но могут быть выделены дополнительные байты, так как реализация может использовать эти байты в качестве контрольных байтов. Какой «размер» я должен пройти до operator delete[] в этом случае? Можете ли вы привести краткий пример использования?

+1

Re: версия массива: это проблема компилятора, а не ваша (вы передаете ей все, что было передано 'operator new []', чтобы выделить эту память). –

+3

Dosen't эта фраза из статьи * Современные распределители памяти часто выделяют в категориях размеров и, по соображениям экономии пространства, не сохраняют размер объекта рядом с объектом. Затем Deallocation требует поиска для хранилища категорий размера, содержащего объект. Этот поиск может быть дорогостоящим, особенно в том случае, когда структуры данных поиска часто не хранятся в кэшах памяти. * Закройте то, что вы спрашиваете? –

+0

@ T.C. Но как я могу узнать, что было передано 'operator new []'? Это просто 'sizeof arr'? – vsoftco

ответ

9

Работа с вашим вторым вопросом: первого

Если присутствует, станд :: size_t аргумент размер должен равняться размером аргумента, передаваемые функции распределения, возвращающих PTR.

Таким образом, любое дополнительное пространство, которое может быть выделено, является обязанностью библиотеки времени выполнения, а не кода клиента.

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

Это также довольно распространено, чтобы не допускать явного явного сохранения размера каждого блока. Распределитель будет часто иметь отдельные пулы для разных размеров блоков (например, полномочия 2 от 16 или около того примерно до пары килобайт или около того). Он будет выделять (справедливый) большой блок из ОС для каждого пула, а затем выделять части этого большого блока пользователю. Когда вы возвращаете адрес, он в основном ищет этот адрес через разные размеры пулов, чтобы найти, из какого пула он пришел. Если у вас много пулов и много блоков в каждом пуле, это может быть относительно медленным.

Идея здесь состоит в том, чтобы избежать обеих этих возможностей. В типичном случае ваши распределения/деаллокация в любом случае более или менее привязаны к стеку, и когда они являются размером, который вы выделяете, скорее всего, будут в локальной переменной. Когда вы освободитесь, вы, как правило, находитесь на (или, по крайней мере, близком) к тому же уровню стека, где и выполняете выделение, так что одна и та же локальная переменная будет легко доступна и, вероятно, не будет выгружена на диск (или что-то в этом роде), так как другие переменные, хранящиеся поблизости, также используются. Для формы без массива вызов ::operator new будет происходить, как правило, из new expression, а звонок - ::operator delete из соответствия delete expression.В этом случае код, созданный для создания/уничтожения объекта, «знает» размер, который он будет запрашивать (и уничтожать), основываясь исключительно на типе создаваемого/уничтожаемого объекта.

+0

Простой «delete p» знает размер выделения (если деструктор 'p' не является полиморфным, он знает его статически), так или иначе. –

+0

Напоминает мне об [разговоре Андрея Александреску на CppCon'15] (https://youtu.be/LIb3L4vKZ7U?list=PLHTh1InhhwT75gykhs7pqcR_uSiG601oh) ... – 5gon12eder

+0

@ 5gon12eder Да, я видел этот разговор, и как он разозлился о новом/удалении:) – vsoftco

4

Для аргумента size для C++ 14 operator delete вы должны передать тот размер, который вы дали operator new, который находится в байтах. Но, как вы обнаружили, это сложнее для массивов. Для почему это более сложная, смотрите здесь: Array placement-new requires unspecified overhead in the buffer?

Так что, если вы сделаете это:

std::string* arr = new std::string[100] 

Это не может быть действительным, чтобы сделать это:

operator delete[](arr, 100 * sizeof(std::string)); # BAD CODE? 

Поскольку первоначальное new выражение было не эквивалент:

std::string* arr = new (new char[100 * sizeof(std::string)]) std::string[100]; 

Что касается того, почему размер delete API лучше, кажется, что today it is actually not, но надеемся, что некоторые стандартные библиотеки улучшат производительность освобождения, потому что они фактически не сохраняют размер выделения рядом с каждым выделенным блоком (классический/учебник модель). Подробнее об этом см. Здесь: Sized Deallocation Feature In Memory Management in C++1y

И, конечно же, причина не хранить размер рядом с каждым распределением заключается в том, что это пустая трата пространства, если вам это действительно не нужно. Для программ, которые делают много небольших динамических распределений (которые более популярны, чем они должны быть!), Эти накладные расходы могут быть значительными. Например, в конструкторе std::shared_ptr «простая ваниль» (вместо make_shared) подсчет ссылок динамически распределяется, поэтому, если ваш распределитель хранит размер рядом с ним, он может наивно требовать около 25% накладных расходов: одно целое число «размер» для распределитель плюс four-slot control block. Не говоря уже о давлении памяти: если размер не хранится рядом с выделенным блоком, вы не загружаете строку из памяти при освобождении - вам нужна только информация, необходимая вам в вызове функции (ну, вам также нужно посмотреть арене или свободном списке или что-то еще, но вам нужно, чтобы в любом случае вы все равно пропустили одну загрузку).

+1

Контрольный блок 'shared_ptr' - это не одно целое число. –

+0

@ T.C .: Хорошая точка, обновленная эта часть. –

+0

«Для размещения в Array-new требуются неопределенные накладные расходы в буфере?» В нем есть определенная серьезная чушь, в частности, в отношении функций выделения мест размещения без выделения ** (игнорировать * распределение * на секунду). 'New (ptr) T [n]' никогда не требовал каких-либо неопределенных накладных расходов, ** он не выделяет, он ничего не делает, он служит только как фиктивный оператор для построения объекта на месте **. Если бы был компилятор, добавляющий эти накладные расходы, это, безусловно, был VC++. – bit2shift

Смежные вопросы