Почему бы заменить операторы нового и удаления по умолчанию?

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

Это продолжение Перегрузка новых и удаление в чрезвычайно освещающем FAQ на С++:
Перегрузка оператора.

Следующее сообщение для этого FAQ:
Как я должен писать стандартные стандартные пользовательские операторы new и delete ISO С++?

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

Ответы

Ответ 1

Можно попытаться заменить операторы new и delete по нескольким причинам, а именно:

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

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

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


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

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

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


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

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

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

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


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

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


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

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


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

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

Ответ 2

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

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

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

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

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


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

Ответ 3

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

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

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

Ответ 4

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

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

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

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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

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