Зачем заменять операторы new и delete по умолчанию?


почему должны можно ли заменить оператор по умолчанию new и delete с пользовательским new и delete операторы?

это в продолжение перегрузки new и delete в чрезвычайно освещающем C++ FAQ:
перегрузка операторов.

следующая запись в этом FAQ:
как я должен написать ISO C++ standard conformant custom new и delete операторы?

Примечание: ответ основан на уроках более эффективного C++Скотта Мейерса.
(Примечание:это должно быть запись в C++ FAQ Stack Overflow. Если вы хотите критиковать идею предоставления FAQ в этой форме, то публикация на meta, которая начала все это было бы место, чтобы сделать это. Ответы на этот вопрос отслеживаются в в C++ чат, где идея FAQ началась в первую очередь, так что ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)

7 61

7 ответов:

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

Для Обнаружения Ошибок Использования:

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

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


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

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

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


Для Сбора Статистики Использования:

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

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

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


для компенсации неоптимального выравнивания памяти в new:

многие компьютерные архитектуры требуют, чтобы данные определенных типов помещались в память по определенным типам адресов. Например, архитектура может требовать, чтобы указатели возникали по адресам, кратным четырем (т. е. были выровнены по четырем байтам), или чтобы двойные указатели возникали по адресам, кратным восьми (т. е. были выровнены по четырем байтам). восемь байт). Несоблюдение таких ограничений может привести к аппаратным исключениям во время выполнения. Другие архитектуры более снисходительны и могут позволить ему работать, хотя и снижая производительность.Оператор new этот корабль с некоторыми компиляторами не гарантирует восьмибайтовое выравнивание для динамического распределение двойников. В таких случаях, замена оператора по умолчанию 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[].

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

вероятно, также стоит упомянуть, что хотя 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++ again2) не обязательно принудительно выравнивать эту большую границу, поэтому, хотя код с использованием распределителя по умолчанию будет работать, замена распределения может обеспечить существенное улучшение скорости для этих операций.


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

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

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

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

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

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

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

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

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

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

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

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

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

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