Какие есть веские причины отказаться std::allocator
от нестандартного решения? Сталкивались ли вы с ситуациями, когда это было абсолютно необходимо для корректности, производительности, масштабируемости и т. Д.? Какие-нибудь действительно умные примеры?
Пользовательские распределители всегда были функцией Стандартной библиотеки, в которой я не особенно нуждался. Мне просто интересно, может ли кто-нибудь здесь на SO предоставить несколько убедительных примеров, чтобы оправдать свое существование.
Одной из областей, где могут быть полезны пользовательские распределители, является разработка игр, особенно на игровых приставках, так как они имеют только небольшой объем памяти и не имеют подкачки. В таких системах вы хотите обеспечить жесткий контроль над каждой подсистемой, чтобы одна некритическая система не могла украсть память у критической. Другие вещи, такие как распределители пула, могут помочь уменьшить фрагментацию памяти. Вы можете найти длинную подробную статью по этой теме по адресу:
EASTL - Стандартная библиотека шаблонов Electronic Arts
источник
Я работаю над mmap-allocator, который позволяет векторам использовать память из отображенного в памяти файла. Цель состоит в том, чтобы векторы, использующие хранилище, находящиеся непосредственно в виртуальной памяти, отображались с помощью mmap. Наша задача - улучшить чтение действительно больших файлов (> 10 ГБ) в память без затрат на копирование, поэтому мне нужен этот специальный распределитель.
Пока у меня есть скелет пользовательского распределителя (который происходит от std :: allocator), я думаю, что это хорошая отправная точка для написания собственных распределителей. Не стесняйтесь использовать этот фрагмент кода любым способом, который вы хотите:
Чтобы использовать это, объявите контейнер STL следующим образом:
Это может быть использовано, например, для регистрации всякий раз, когда выделяется память. Что необходимо, так это структура переброса, иначе векторный контейнер использует методы выделения / освобождения суперклассов.
Обновление: распределитель памяти теперь доступен по адресу https://github.com/johannesthoma/mmap_allocator и является LGPL. Не стесняйтесь использовать его для своих проектов.
источник
Я работаю с механизмом хранения MySQL, который использует c ++ для своего кода. Мы используем собственный распределитель, чтобы использовать систему памяти MySQL, а не конкурировать с MySQL за память. Это позволяет нам убедиться, что мы используем память как пользователь, настроенный для использования MySQL, а не как «лишние».
источник
Может быть полезно использовать пользовательские распределители для использования пула памяти вместо кучи. Это один пример среди многих других.
В большинстве случаев это, безусловно, преждевременная оптимизация. Но это может быть очень полезно в определенных контекстах (встроенные устройства, игры и т. Д.).
источник
Я не написал код C ++ с пользовательским распределителем STL, но могу представить веб-сервер, написанный на C ++, который использует специальный распределитель для автоматического удаления временных данных, необходимых для ответа на HTTP-запрос. Пользовательский распределитель может освободить все временные данные сразу после генерации ответа.
Другой возможный вариант использования пользовательского распределителя (который я использовал) - написание модульного теста, чтобы доказать, что поведение функции не зависит от какой-либо части ее ввода. Пользовательский распределитель может заполнить область памяти любым шаблоном.
источник
При работе с графическими процессорами или другими сопроцессорами иногда выгодно выделять структуры данных в основной памяти особым образом . Этот особый способ выделения памяти может быть реализован в пользовательском распределителе удобным способом.
Причина, по которой настраиваемое выделение во время выполнения ускорителя может быть выгодным при использовании ускорителей, заключается в следующем:
источник
Я использую пользовательские распределители здесь; Вы могли бы даже сказать, что это обходило другое настраиваемое управление динамической памятью.
Предыстория: у нас есть перегрузки для malloc, calloc, free и различных вариантов операторов new и delete, и компоновщик с радостью заставляет STL использовать их для нас. Это позволяет нам делать такие вещи, как автоматический пул небольших объектов, обнаружение утечек, выделение заполнений, свободное заполнение, распределение заполнения с помощью часовых, выравнивание строк кэша для определенных распределений и освобождение с задержкой.
Проблема в том, что мы работаем во встроенной среде - вокруг недостаточно памяти для правильного учета утечек в течение длительного периода времени. По крайней мере, не в стандартной оперативной памяти - есть еще одна куча оперативной памяти, доступная в другом месте, с помощью пользовательских функций выделения.
Решение: написать собственный распределитель, который использует расширенную кучу, и использовать его только во внутренностях архитектуры отслеживания утечек памяти ... Все остальное по умолчанию - обычные перегрузки new / delete, которые отслеживают утечки. Это позволяет избежать самого отслеживания трекера (и также предоставляет немного дополнительных функций упаковки, мы знаем размер узлов трекера).
Мы также используем это для сохранения данных профилирования стоимости функций по той же причине; Написание записи для каждого вызова функции и возврата, а также переключателей потоков может быстро стать дорогим. Пользовательский распределитель снова дает нам меньшие ресурсы в большей области отладочной памяти.
источник
Я использую собственный распределитель для подсчета количества выделений / освобождений в одной части моей программы и измерения того, сколько времени это займет. Есть и другие способы, которыми это может быть достигнуто, но этот метод очень удобен для меня. Особенно полезно, что я могу использовать собственный распределитель только для подмножества моих контейнеров.
источник
Одна существенная ситуация: при написании кода, который должен работать через границы модуля (EXE / DLL), важно, чтобы ваши выделения и удаления происходили только в одном модуле.
Там я столкнулся с плагинной архитектурой в Windows. Важно, например, что если вы передаете std :: string через границу DLL, любые перераспределения строки происходят из кучи, откуда она возникла, а не из кучи в DLL, которая может отличаться *.
* На самом деле это сложнее, чем если бы вы динамически связывались с CRT, это может сработать в любом случае. Но если каждая DLL имеет статическую ссылку на CRT, вы отправляетесь в мир боли, где постоянно возникают ошибки размещения фантомов.
источник
Одним из примеров того, как я использовал их, была работа со встроенными системами с очень ограниченными ресурсами. Допустим, у вас есть 2 КБ оперативной памяти, и ваша программа должна использовать часть этой памяти. Вы должны хранить, скажем, 4-5 последовательностей где-то, чего нет в стеке, и, кроме того, вам нужен очень точный доступ к тому, где хранятся эти вещи, это ситуация, когда вы можете написать свой собственный распределитель. Реализации по умолчанию могут фрагментировать память, это может быть неприемлемо, если у вас недостаточно памяти и вы не можете перезапустить вашу программу.
Одним из проектов, над которым я работал, было использование AVR-GCC на некоторых маломощных чипах. Нам пришлось хранить 8 последовательностей переменной длины, но с известным максимумом. Стандартная реализация библиотеки управления памятьюявляется тонкой оберткой вокруг malloc / free, которая отслеживает, куда помещать элементы, добавляя каждый выделенный блок памяти указателем к концу этого выделенного фрагмента памяти. При выделении нового фрагмента памяти стандартный распределитель должен пройтись по каждому из фрагментов памяти, чтобы найти следующий доступный блок, в который поместится запрошенный объем памяти. На настольной платформе это будет очень быстро для этих нескольких элементов, но вы должны иметь в виду, что некоторые из этих микроконтроллеров очень медленны и примитивны по сравнению. Кроме того, проблема фрагментации памяти была серьезной проблемой, которая означала, что у нас действительно не было выбора, кроме как выбрать другой подход.
Так что мы сделали, чтобы реализовать наш собственный пул памяти . Каждый блок памяти был достаточно большим, чтобы вместить в себя самую большую последовательность, в которой мы нуждались. Это заранее выделило блоки памяти фиксированного размера и пометило, какие блоки памяти использовались в настоящее время. Мы сделали это, сохранив одно 8-битное целое число, где каждый бит представлен, если используется определенный блок. Здесь мы обменяли использование памяти на попытки ускорить весь процесс, что в нашем случае было оправданным, поскольку мы приближали этот микроконтроллер к максимальной его вычислительной мощности.
В ряде случаев я вижу, как пишу свой собственный распределитель в контексте встроенных систем, например, если память для последовательности не находится в основном оперативной памяти, как это часто бывает на этих платформах .
источник
Обязательная ссылка на доклад Андрея Александреску на CppCon 2015 о распределителях:
https://www.youtube.com/watch?v=LIb3L4vKZ7U
Приятно то, что их разработка заставляет задуматься о том, как их использовать :-)
источник
Для разделяемой памяти жизненно важно, чтобы не только заголовок контейнера, но и содержащиеся в нем данные сохранялись в разделяемой памяти.
Распределитель Boost :: Interprocess является хорошим примером. Однако, как вы можете прочитать здесь, этого всего недостаточно, чтобы совместить совместную память всех контейнеров STL (из-за разных смещений отображения в разных процессах указатели могут «сломаться»).
источник
Некоторое время назад я нашел это решение очень полезным для меня: быстрый распределитель C ++ 11 для контейнеров STL . Это немного ускоряет контейнеры STL на VS2017 (~ 5x), а также на GCC (~ 7x). Это распределитель специального назначения, основанный на пуле памяти. Он может использоваться с контейнерами STL только благодаря запрашиваемому вами механизму.
источник
Я лично использую Loki :: Allocator / SmallObject для оптимизации использования памяти для небольших объектов - он показывает хорошую эффективность и удовлетворительную производительность, если вам приходится работать с умеренным количеством действительно небольших объектов (от 1 до 256 байт). Это может быть примерно в 30 раз более эффективно, чем стандартное распределение C ++ new / delete, если мы говорим о выделении умеренного количества небольших объектов разных размеров. Кроме того, существует решение для VC под названием «QuickHeap», оно обеспечивает наилучшую возможную производительность (операции выделения и освобождения просто читают и записывают адрес блока, который выделяется / возвращается в кучу, соответственно, в 99%. (9)% случаев - зависит от настроек и инициализации), но за счет значительных накладных расходов - для него требуется два указателя на экстент и один дополнительный для каждого нового блока памяти. Это'
Проблема со стандартной реализацией new / delete в C ++ заключается в том, что обычно это просто оболочка для выделения памяти malloc / free, и она хорошо работает для больших блоков памяти, таких как 1024+ байта. Он имеет заметные накладные расходы с точки зрения производительности, а иногда и дополнительной памяти, используемой для отображения. Таким образом, в большинстве случаев пользовательские распределители реализованы таким образом, чтобы максимизировать производительность и / или минимизировать объем дополнительной памяти, необходимой для выделения небольших (≤1024 байтов) объектов.
источник
В графической симуляции я видел пользовательские распределители, используемые для
std::allocator
не поддерживаются напрямую.источник