Как использовать операторы размера delete/delete [] и почему они лучше?

С++ 14 представил "размерные" версии operator 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

В первую очередь: второй вопрос:

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

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

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

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

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

Ответ 2

Для аргумента size для С++ 14 operator delete вы должны передать тот же размер, который вы указали в operator new, который находится в байтах. Но поскольку вы обнаружили это более сложным для массивов. Для чего это сложнее, см. Здесь: Размещение массива-new требует неопределенных служебных данных в буфере?

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

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 лучше, кажется, что сегодня это фактически не, но есть надежда, что некоторые стандартные библиотеки улучшат производительность освобождения, поскольку на самом деле они не сохраняют размер распределения рядом с каждым выделенным блоком (модель классического/учебника). Подробнее об этом см. Здесь: Функция расширенного освобождения в управлении памятью в С++ 1y

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