Почему бы заменить по умолчанию новые и удалить операторы? - PullRequest
64 голосов
/ 22 августа 2011

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

Это продолжение Перегрузка нового и удаление в чрезвычайно ярком C ++ FAQ:
Перегрузка оператора.

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

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

Ответы [ 7 ]

66 голосов
/ 22 августа 2011

Можно попытаться заменить операторы 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, который перезаписывает освобожденную память нулями, чтобы повысить безопасность данных приложения.

12 голосов
/ 22 августа 2011

Во-первых, действительно существует несколько различных операторов 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[] отделены друг от друга (аналогично для любого * 1020)* и 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 - я сомневаюсь, что у него необычное количество таких проблем, но я часто его используюпоэтому я склонен быть в курсе его проблем.
6 голосов
/ 22 августа 2011

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

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

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

4 голосов
/ 22 августа 2011

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

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

4 голосов
/ 22 августа 2011

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

3 голосов
/ 04 мая 2013

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

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

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

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

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

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

...