Любая причина перегрузить глобальное новое и удалить?
Если вы не программируете части ОС или встроенной системы, есть ли причины для этого? Я могу себе представить, что для некоторых конкретных классов, которые создаются и уничтожаются, частая перегрузка функций управления памятью или введение пула объектов может снизить накладные расходы, но делать это глобально?
прибавление
Я только что нашел ошибку в перегруженной функции удаления - память не всегда освобождалась. И это было не так критично для памяти. Кроме того, отключение этих перегрузок снижает производительность только на ~0,5%.
15 ответов
Мы перегружаем глобальные операторы new и delete, где я работаю по многим причинам:
- объединение всех небольших выделенных ресурсов - уменьшает накладные расходы, уменьшает фрагментацию, может повысить производительность для приложений с небольшим распределением ресурсов
- распределениекадров с известным временем жизни - игнорируйте все освобождения до самого конца этого периода, затем освободите их все вместе (по общему признанию, мы делаем это больше с перегрузками локальных операторов, чем с глобальными)
- корректировкавыравнивания - по границам кэширования и т. д.
- alloc fill - помогает разоблачить использование неинициализированных переменных
- free fill - помогает разоблачить использование ранее удаленной памяти
- бесплатная задержка - повышение эффективности бесплатного заполнения, иногда повышение производительности
- часовые или ограждения - помогают выявить переполнение буфера, недополнение и иногда дикий указатель
- перенаправление выделений - для учета NUMA, специальных областей памяти или даже для разделения отдельных систем в памяти (например, для встроенных языков сценариев или DSL)
- сборка мусора или очистка - снова полезно для этих встроенных языков сценариев
- проверка кучи - вы можете пройтись по структуре данных кучи, которую каждый N выделяет / освобождает, чтобы убедиться, что все выглядит хорошо
- учет, включая отслеживание утечек и снимки / статистику использования (стеки, возраст распределения и т. д.)
Идея нового / удаленного учета действительно гибкая и мощная: вы можете, например, записывать весь стек вызовов для активного потока всякий раз, когда происходит выделение ресурсов, и собирать статистические данные об этом. Вы можете отправить информацию о стеке по сети, если у вас нет места для ее локального хранения по какой-либо причине. Типы информации, которую вы можете собрать здесь, ограничены только вашим воображением (и, конечно, производительностью).
Мы используем глобальные перегрузки, потому что там удобно вешать множество общих функций отладки, а также вносить радикальные улучшения во всё приложение, основываясь на статистике, которую мы собираем по тем же перегрузкам.
Мы все еще используем пользовательские распределители для отдельных типов; во многих случаях ускорение или возможности, которые вы можете получить, предоставляя настраиваемые распределители, например, для одной точки использования структуры данных STL, значительно превышают общее ускорение, которое вы можете получить от глобальных перегрузок.
Взгляните на некоторые из распределителей и систем отладки, которые существуют для C/C++, и вы быстро придумаете эти и другие идеи:
- Valgrind
- забор под напряжением
- пооддержки
- dlmalloc
- Проверка приложения
- Застраховать ++
- BoundsChecker
- ... и многие другие... (индустрия gamedev - отличное место для поиска)
(Одна старая, но оригинальная книга " Написание твердого кода", в которой обсуждаются многие причины, по которым вы можете захотеть предоставить пользовательские распределители в C, большинство из которых по-прежнему очень актуальны.)
Очевидно, что если вы можете использовать любой из этих прекрасных инструментов, вы захотите сделать это, а не использовать свои собственные.
Бывают ситуации, когда это быстрее, проще, меньше хлопот с бизнесом / юриспруденцией, пока нет ничего доступного для вашей платформы или просто более поучительно: покопайтесь и напишите глобальную перегрузку.
Наиболее распространенная причина перегрузки new и delete заключается в простой проверке утечек памяти и статистики использования памяти. Обратите внимание, что "утечка памяти" обычно обобщается на ошибки памяти. Вы можете проверить такие вещи, как двойное удаление и переполнение буфера.
После этого обычно используются схемы выделения памяти, такие как сборка мусора и создание пулов.
Все остальные случаи - это просто конкретные вещи, упомянутые в других ответах (запись на диск, использование ядра).
В дополнение к другим важным применениям, упомянутым здесь, таким как тегирование памяти, это также единственный способ заставить все выделения в вашем приложении проходить выделение по фиксированным блокам, что имеет огромное значение для производительности и фрагментации.
Например, у вас может быть серия пулов памяти с фиксированными размерами блоков. Переопределение глобального new
позволяет вам направить все 61-байтовое распределение, скажем, в пул с 64-байтовыми блоками, все 768-1024 байта выделяются в пул 1024b-блоков, все вышеупомянутые - в пул 2048-байтовых блоков, и все, что больше 8 КБ к общей рваной куче.
Поскольку фиксированные распределители блоков намного быстрее и менее подвержены фрагментации, чем произвольное выделение из кучи, это позволяет принудительно распределять даже дурацкий сторонний код из ваших пулов, а не распределять по всему адресному пространству.
Это часто делается в системах, критически важных для времени и пространства, таких как игры. 280Z28, Ми, и Дэн Олсон описали почему.
UnrealEngine3 перегружает глобальные новые и удаляет как часть своей основной системы управления памятью. Существует несколько распределителей, которые предоставляют различные функции (профилирование, производительность и т. Д.), И им требуются все выделения для его прохождения.
Изменить: Для моего собственного кода, я бы сделал это только в крайнем случае. И под этим я подразумеваю, что я почти положительно никогда не использовал бы это. Но мои личные проекты, очевидно, намного меньше / очень разные требования.
Некоторые системы реального времени перегружают их, чтобы избежать их использования после инициализации.
Перегрузка new & delete позволяет добавить тег в ваши выделения памяти. Я отмечаю распределение для системы или элемента управления или промежуточного программного обеспечения. Я могу посмотреть во время выполнения, сколько каждый использует. Может быть, я хочу увидеть использование парсера отдельно от пользовательского интерфейса или сколько реально используется промежуточного программного обеспечения!
Вы также можете использовать его для размещения защитных полос вокруг выделенной памяти. Если / когда ваше приложение дает сбой, вы можете взглянуть на адрес. Если вы видите содержимое как "0xABCDABCD" (или что-то другое, что вы выбираете в качестве защиты), вы получаете доступ к памяти, которой вы не владеете.
Возможно, после вызова delete вы можете заполнить это пространство аналогичным образом. Я считаю, что VisualStudio делает что-то подобное в отладке. Разве он не заполняет неинициализированную память 0xCDCDCDCD?
Наконец, если у вас есть проблемы фрагментации, вы можете использовать его для перенаправления на распределитель блоков? Я не уверен, как часто это действительно проблема.
Вам нужно перегружать их, когда вызов new и delete не работает в вашей среде.
Например, в программировании ядра стандартные значения new и delete не работают, так как они используют библиотеку пользовательского режима для выделения памяти.
Это может быть хорошим трюком для вашего приложения, чтобы иметь возможность реагировать на нехватку памяти чем-то другим, кроме случайного сбоя. Чтобы сделать это ваш new
может быть простой прокси по умолчанию new
который ловит свои неудачи, освобождает некоторые вещи и пытается снова.
Самая простая техника - зарезервировать пустой блок памяти во время запуска для этой цели. У вас также может быть некоторый кеш, к которому вы можете подключиться - идея та же.
Когда первый сбой распределения начинается, у вас все еще есть время, чтобы предупредить вашего пользователя о нехватке памяти ("Я смогу выжить немного дольше, но вы можете сохранить свою работу и закрыть некоторые другие приложения"), сохраните свое состояние на диск, переключитесь в режим выживания или что-либо еще, что имеет смысл в вашем контексте.
Я видел это сделано в системе, которая для "безопасности"*
необходимо было перезаписать всю память, которая использовалась при выделении памяти. Подход заключался в том, чтобы выделить несколько дополнительных байтов в начале каждого блока памяти, который будет содержать размер всего блока, который затем будет перезаписан нулями при удалении.
Это, как вы можете себе представить, имело ряд проблем, но это сработало (в основном) и спасло группу от проверки каждого выделения памяти в достаточно большом существующем приложении.
Конечно, я не говорю, что это хорошее применение, но, вероятно, оно является одним из наиболее творческих...
*
к сожалению, речь шла не столько о реальной безопасности, сколько о ее безопасности...
Я сделал это с отображенными в память файлами, чтобы данные, записанные в память, автоматически также сохранялись на диск.
Он также используется для возврата памяти по определенному физическому адресу, если у вас есть устройства ввода-вывода с отображенной памятью, или иногда, если вам нужно выделить определенный блок непрерывной памяти.
Но в 99% случаев это делается как функция отладки, чтобы регистрировать, как часто, где, когда выделяется и освобождается память.
Плагины Photoshop, написанные на C++, должны переопределить operator new
так что они получают память через Photoshop.
На самом деле в играх довольно часто выделяют один огромный кусок памяти из системы, а затем предоставляют пользовательские распределители через перегруженные new и delete. Одна из основных причин заключается в том, что консоли имеют фиксированный объем памяти, что делает как утечки, так и фрагментацию большими проблемами.
Обычно (по крайней мере, на закрытой платформе) операции кучи по умолчанию сопровождаются отсутствием контроля и самоанализа. Для многих приложений это не имеет значения, но для стабильной работы игр в условиях фиксированной памяти дополнительный контроль и самоанализ чрезвычайно важны.
С практической точки зрения может быть лучше переопределить malloc на уровне системной библиотеки, поскольку оператор new, вероятно, будет вызывать его в любом случае.
В linux вы можете поместить свою собственную версию malloc вместо системной, как в следующем примере:
http://developers.sun.com/solaris/articles/lib_interposers.html
В этой статье они пытаются собрать статистику производительности. Но вы также можете обнаружить утечки памяти, если вы также переопределите free.
Поскольку вы делаете это в разделяемой библиотеке с LD_PRELOAD, вам даже не нужно перекомпилировать ваше приложение.
Наиболее распространенный вариант использования - это, вероятно, проверка на утечку.
Другой вариант использования - когда у вас есть особые требования к выделению памяти в вашей среде, которые не удовлетворяются используемой вами стандартной библиотекой, как, например, вы должны гарантировать, что выделение памяти не блокируется в многопоточной среде.
Перегруженный новый оператор также позволяет программистам выжать из своих программ некоторую дополнительную производительность. Например, в классе для ускорения выделения новых узлов поддерживается список удаленных узлов, так что их память может быть повторно использована при выделении новых узлов. В этом случае перегруженный оператор удаления добавит узлы в список удаленных узлов и перегруженный новый оператор будет выделять память из этого списка, а не из кучи для ускорения выделения памяти. Память из кучи может быть использована, когда список удаленных узлов пуст.
Как уже говорили многие, это обычно делается в приложениях, критичных к производительности, или для того, чтобы иметь возможность управлять выравниванием памяти или отслеживать вашу память. В играх часто используются собственные диспетчеры памяти, особенно при работе с конкретными платформами / консолями.
Вот довольно хороший пост в блоге об одном способе сделать это и некоторые рассуждения.