Why would one replace default new and delete operators?

Why should would one replace the default operator new а также delete with a custom new а также delete operators?

This is in continuation of Overloading new and delete in the immensely illuminating C++ FAQ:
Operator overloading.

An followup entry to this FAQ is:
How should I write ISO C++ standard conformant custom new а также delete operators?

Note: The answer is based on lessons from Scott Meyers' More Effective C++.
(Примечание. Предполагается, что это будет вход в FAQ по C++ в Stack Overflow. Если вы хотите критиковать идею предоставления FAQ в этой форме, то публикация в meta, с которой все это началось, будет подходящим местом для этого. этот вопрос отслеживается в чате C++, где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)

7 ответов

Решение

Можно попробовать заменить new а также delete операторы по ряду причин, а именно:

Чтобы обнаружить ошибки использования:

Существует несколько способов неправильного использования new а также delete может привести к ужасным зверям из-за неопределенного поведения и утечек памяти. Соответствующие примеры каждого из них:
Использование более одного delete на newпамять и не звонить delete на памяти, выделенной с помощью new,
Перегруженный оператор new может хранить список выделенных адресов и перегруженный оператор delete Можно удалить адреса из списка, тогда легко обнаружить такие ошибки использования.

Точно так же различные ошибки программирования могут привести к переполнению данных(запись за пределами выделенного блока) и недополнению(запись до начала выделенного блока).
Перегруженный оператор new может перераспределить блоки и поместить известные байтовые шаблоны ("подписи") до и после того, как память станет доступной для клиентов. Перегруженный оператор удаления может проверить, не повреждены ли подписи. Таким образом, проверяя, не являются ли эти сигнатуры целыми, можно определить, что переполнение или недостаточный запуск произошли когда-то в течение срока службы выделенного блока, и оператор delete может зарегистрировать этот факт вместе со значением указателя-нарушителя, тем самым помогая в предоставлении хорошей диагностической информации.


Для повышения эффективности (скорость и память):

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

Если вы хорошо разбираетесь в моделях использования динамической памяти вашей программы, вы часто можете обнаружить, что пользовательские версии оператора new и оператора delete превосходят (быстрее по производительности или требуют меньше памяти до 50%) версии по умолчанию. Конечно, если вы не уверены в том, что делаете, это не очень хорошая идея (даже не пытайтесь делать это, если не понимаете сложностей).


Чтобы собрать статистику использования:

Прежде чем думать о замене new а также delete для повышения эффективности, как указано в #2, вы должны собрать информацию о том, как ваше приложение / программа использует динамическое распределение. Вы можете собирать информацию о:
Распределение блоков размещения,
Распределение жизней,
Порядок распределения (FIFO или LIFO или случайный),
Понимание характера использования в течение определенного периода времени, максимального объема используемой динамической памяти и т. Д.

Кроме того, иногда вам может понадобиться собрать информацию об использовании, такую ​​как:
Подсчитать количество динамически объектов класса,
Ограничьте количество создаваемых объектов с помощью динамического размещения и т. Д.

Все, эта информация может быть собрана путем замены пользовательских new а также delete и добавление механизма сбора диагностики в перегруженный new а также delete,


Чтобы компенсировать неоптимальное выравнивание памяти в new:

Многие компьютерные архитектуры требуют, чтобы данные определенных типов помещались в память по определенным адресам. Например, архитектура может требовать, чтобы указатели встречались по адресам, кратным четырем (т. Е. Выровненным по четырем байтам), или чтобы удвоения должны происходить по адресам, кратным восьми (т. Е. Выровненным по восьмибайтовым значениям). Несоблюдение таких ограничений может привести к аппаратным исключениям во время выполнения. Другие архитектуры более щадящие и могут позволить работать, хотя и снижают производительность. Оператор new этот комплект с некоторыми компиляторами не гарантирует восьмибайтовое выравнивание для динамического распределения значений типа double. В таких случаях замена оператора по умолчанию new с тем, который гарантирует восьмибайтовое выравнивание, может привести к значительному увеличению производительности программы и может стать хорошей причиной для замены new а также delete операторы.


Чтобы сгруппировать связанные объекты рядом друг с другом:

Если вы знаете, что определенные структуры данных обычно используются вместе, и вы хотите минимизировать частоту сбоев страниц при работе с данными, может иметь смысл создать отдельную кучу для структур данных, чтобы они были сгруппированы вместе на нескольких страницы, насколько это возможно. Пользовательские версии размещения new а также delete может сделать возможным достижение такой кластеризации.


Чтобы получить нетрадиционное поведение:

Иногда вы хотите, чтобы операторы new и delete делали то, чего не предоставляют версии, предоставляемые компилятором.
Например: вы можете написать собственный оператор delete это перезаписывает освобожденную память нулями, чтобы повысить безопасность данных приложения.

Во-первых, действительно есть ряд различных new а также delete операторы (произвольное число, действительно).

Во-первых, есть ::operator new, ::operator new[], ::operator delete а также ::operator delete[], Во-вторых, для любого класса X, имеются X::operator new, X::operator new[], X::operator delete а также X::operator delete[],

Между ними гораздо более распространено перегружать операторы, специфичные для класса, чем глобальные операторы - для использования памяти определенного класса довольно характерно следовать достаточно конкретному шаблону, чтобы вы могли писать операторы, которые обеспечивают существенные улучшения по умолчанию. Как правило, гораздо сложнее точно или конкретно предсказать использование памяти в глобальном масштабе.

Вероятно, стоит упомянуть, что хотя operator new а также operator new[] отделены друг от друга (аналогично для любого X::operator new а также X::operator new[]), нет никакой разницы между требованиями к двум. Один будет вызываться для выделения одного объекта, а другой - для выделения массива объектов, но каждый из них все равно просто получает необходимый объем памяти и должен возвращать адрес блока памяти (по крайней мере) такого большого размера.

Говоря о требованиях, вероятно, стоит рассмотреть другие требования1: глобальные операторы должны быть действительно глобальными - вы не можете помещать их в пространство имен или делать один статический в определенной единице перевода. Другими словами, существует только два уровня, на которых могут иметь место перегрузки: перегрузка, специфичная для класса, или глобальная перегрузка. Промежуточные точки, такие как "все классы в пространстве имен X" или "все распределения в единице перевода Y", не допускаются. Операторы класса должны быть static - но на самом деле вы не обязаны объявлять их как статические - они будут статичными, независимо от того, объявите ли вы их явно static или нет. Официально глобальные операторы много возвращают память, выровненную так, что она может использоваться для объекта любого типа. Неофициально, есть небольшая простор в одном отношении: если вы получаете запрос для небольшого блока (например, 2 байта), вам действительно нужно предоставить память, выровненную для объекта до этого размера, так как попытка сохранить там что-то большее в любом случае приведет к неопределенному поведению.

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

Так как это более распространено, я сначала расскажу о классовых операторах. Основной причиной управления памятью для конкретного класса является производительность. Обычно это происходит в одной или двух формах: либо повышение скорости, либо уменьшение фрагментации. Скорость повышается за счет того, что диспетчер памяти будет работать только с блоками определенного размера, поэтому он может возвращать адрес любого свободного блока, а не тратить время на проверку того, достаточно ли велик блок, разбивая блок на две части, если он слишком большой и т. д. Фрагментация уменьшается (в основном) таким же образом - например, предварительное выделение блока, достаточно большого для N объектов, дает ровно пространство, необходимое для N объектов; выделение памяти для одного объекта выделит ровно место для одного объекта, а не на один байт больше.

Существует множество причин перегрузки операторов глобального управления памятью. Многие из них ориентированы на отладку или инструментарий, такой как отслеживание общего объема памяти, необходимой приложению (например, при подготовке к переносу во встроенную систему), или отладка проблем с памятью путем выявления несоответствий между выделением и освобождением памяти. Другой распространенной стратегией является выделение дополнительной памяти до и после границ каждого запрашиваемого блока и запись уникальных шаблонов в эти области. В конце выполнения (и, возможно, в другое время) эти области проверяются, чтобы определить, не был ли код написан за пределами выделенных границ. Еще одно - попытаться улучшить простоту использования за счет автоматизации, по крайней мере, некоторых аспектов выделения или удаления памяти, например, с помощью автоматического сборщика мусора.

Глобальный распределитель не по умолчанию также может быть использован для повышения производительности. Типичным случаем будет замена распределителя по умолчанию, который в целом был медленным (например, по крайней мере некоторые версии MS VC++ около 4.x будут вызывать систему HeapAlloc а также HeapFree функции для каждой операции выделения / удаления). Другая возможность, которую я видел на практике, произошла на процессорах Intel при использовании операций SSE. Они работают на 128-битных данных. Хотя операции будут работать независимо от выравнивания, скорость улучшается, когда данные выровнены по 128-битным границам. Некоторые компиляторы (например, снова MS VC++2) не обязательно приводят в соответствие выравнивание с этой большей границей, поэтому даже если код, использующий распределитель по умолчанию, будет работать, замена выделения может обеспечить существенное улучшение скорости для этих операций.


  1. Большинство требований покрыто в §3.7.3 и §18.4 стандарта C++ (или в §3.7.4 и §18.6 в C++0x, по крайней мере, начиная с N3291).
  2. Я чувствую себя обязанным отметить, что я не собираюсь выбирать компилятор Microsoft - я сомневаюсь, что у него необычное количество таких проблем, но я часто его использую, поэтому я, как правило, хорошо осведомлен о его проблемах.

Кажется, стоит повторить список из моего ответа от "Есть ли причина перегрузить глобальное новое и удалить?" здесь - посмотрите этот ответ (или другие ответы на этот вопрос) для более подробного обсуждения, ссылок и других причин. Эти причины обычно относятся к локальным перегрузкам операторов, а также к перегрузкам по умолчанию / глобальным и к Cmalloc/calloc/realloc/free перегрузки или крюки, а также.

Мы перегружаем глобальные операторы new и delete, где я работаю по многим причинам:

  • объединение всех небольших выделенных ресурсов - уменьшает накладные расходы, уменьшает фрагментацию, может повысить производительность для приложений с небольшим распределением ресурсов
  • распределениекадров с известным временем жизни - игнорируйте все освобождения до самого конца этого периода, затем освободите их все вместе (по общему признанию, мы делаем это больше с перегрузками локальных операторов, чем с глобальными)
  • корректировкавыравнивания - по границам кэширования и т. д.
  • alloc fill - помогает разоблачить использование неинициализированных переменных
  • free fill - помогает разоблачить использование ранее удаленной памяти
  • бесплатная задержка - повышение эффективности бесплатного заполнения, иногда повышение производительности
  • часовые или ограждения - помогают выявить переполнение буфера, недополнение и иногда дикий указатель
  • перенаправление выделений - для учета NUMA, специальных областей памяти или даже для разделения отдельных систем в памяти (например, для встроенных языков сценариев или DSL)
  • сборка мусора или очистка - снова полезно для этих встроенных языков сценариев
  • проверка кучи - вы можете пройтись по структуре данных кучи, которую каждый N выделяет / освобождает, чтобы убедиться, что все выглядит хорошо
  • учет, включая отслеживание утечек и снимки / статистику использования (стеки, возраст распределения и т. д.)

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

Чтобы уточнить: если архитектура требует, например, что double данные должны быть выровнены на восемь байтов, тогда оптимизировать нечего. Любой вид динамического выделения соответствующего размера (например, malloc(size), operator new(size), operator new[](size), new char[size] где size >= sizeof(double)) гарантированно будет правильно выровнен. Если реализация не дает этой гарантии, она не соответствует. изменения operator new сделать "правильную вещь" в этом случае было бы попыткой "исправить" реализацию, а не оптимизацией.

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

Оператор new, который поставляется с некоторыми компиляторами, не гарантирует восьмибайтовое выравнивание для динамического распределения значений типа double.

Цитировать, пожалуйста. Обычно новый оператор по умолчанию является лишь немного более сложным, чем оболочка malloc, которая, по стандарту, возвращает память, соответствующим образом выровненную для ЛЮБОГО типа данных, поддерживаемого целевой архитектурой.

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

Связанные со статистикой использования: бюджетирование по подсистемам. Например, в консольной игре вам может потребоваться зарезервировать некоторую часть памяти для геометрии 3D-модели, некоторую для текстур, некоторую для звуков, некоторую для игровых сценариев и т. Д. Пользовательские распределители могут пометить каждое выделение подсистемой и выпустить предупреждение, когда отдельные бюджеты превышены.

Я использовал его для выделения объектов в определенной области общей памяти. (Это похоже на то, что упоминал @Russell Borogove.)

Несколько лет назад я разработал программное обеспечение для CAVE. Это многостенная система VR. Он использовал один компьютер для управления каждым проектором; 6 был максимум (4 стены, пол и потолок), в то время как 3 был более распространенным (2 стены и пол). Машины общались через специальное оборудование с общей памятью.

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

Другие вопросы по тегам