Принудительные примеры пользовательских дистрибутивов С++?

Каковы некоторые действительно веские причины, чтобы опрокинуть std::allocator в пользу пользовательского решения? Вы сталкиваетесь с ситуациями, когда это абсолютно необходимо для правильности, производительности, масштабируемости и т.д.? Какие-нибудь действительно умные примеры?

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

Ответ 1

Как я упоминаю здесь, я видел, что специализированный STL-распределитель Intel TBB значительно повышает производительность многопоточного приложения, просто изменив один

std::vector<T>

to

std::vector<T,tbb::scalable_allocator<T> >

(это быстрый и удобный способ переключения распределителя на использование избыточных нитей, связанных с TBB, см. стр. 7 в этом документе)

Ответ 2

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

EASTL - стандартная библиотека шаблонов Electronic Arts

Ответ 3

Я работаю над mmap-allocator, который позволяет векторам использовать память из файл с отображением памяти. Цель состоит в том, чтобы иметь векторы, которые используют хранилище, которое находятся непосредственно в виртуальной памяти, отображаемой mmap. Наша проблема заключается в том, чтобы улучшить чтение действительно больших файлов ( > 10 ГБ) в память без копирования накладные расходы, поэтому мне нужен этот настраиваемый распределитель.

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

#include <memory>
#include <stdio.h>

namespace mmap_allocator_namespace
{
        template <typename T>
        class mmap_allocator: public std::allocator<T>
        {
public:
                typedef size_t size_type;
                typedef T* pointer;
                typedef const T* const_pointer;

                template<typename _Tp1>
                struct rebind
                {
                        typedef mmap_allocator<_Tp1> other;
                };

                pointer allocate(size_type n, const void *hint=0)
                {
                        fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T));
                        return std::allocator<T>::allocate(n, hint);
                }

                void deallocate(pointer p, size_type n)
                {
                        fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p);
                        return std::allocator<T>::deallocate(p, n);
                }

                mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
                mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { }
                template <class U>                    
                mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { }
                ~mmap_allocator() throw() { }
        };
}

Чтобы использовать это, объявите контейнер STL следующим образом:

using namespace std;
using namespace mmap_allocator_namespace;

vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>());

Его можно использовать, например, для регистрации при распределении памяти. Что необходимо - это структура повторной привязки, иначе векторный контейнер использует суперклассы allocate/deallocate Методы.

Обновление. Распределитель отображения памяти теперь доступен в https://github.com/johannesthoma/mmap_allocator и является LGPL. Не стесняйтесь использовать его для своих проектов.

Ответ 4

Я работаю с механизмом хранения MySQL, который использует С++ для своего кода. Мы используем настраиваемый распределитель для использования системы памяти MySQL, а не для сопоставления с MySQL для памяти. Это позволяет нам убедиться, что мы используем память, поскольку пользователь настроил MySQL для использования, а не "лишний".

Ответ 5

Полезно использовать пользовательские распределители для использования пула памяти вместо кучи. Этот пример среди многих других.

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

Ответ 6

Я не написал код на С++ с помощью специализированного распределителя STL, но могу представить себе веб-сервер, написанный на С++, который использует специальный распределитель для автоматического удаления временных данных, необходимых для ответа на HTTP-запрос. Пользовательский распределитель может освобождать все временные данные сразу после получения ответа.

Другой возможный случай использования для специального распределителя (который я использовал) пишет unit test, чтобы доказать, что поведение функции не зависит от какой-либо части ее ввода. Пользовательский распределитель может заполнить область памяти любым шаблоном.

Ответ 7

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

Предыстория: у нас есть перегрузки для malloc, calloc, free и различные варианты оператора new и delete, и компоновщик с радостью делает это STL для них. Это позволяет нам делать такие вещи, как автоматическое объединение мелких объектов, обнаружение утечек, заполнение адресатов, свободное заполнение, распределение дополнений с часовыми приборами, выравнивание кэш-строки для определенных распределений и отсрочка.

Проблема в том, что мы работаем во встроенной среде - недостаточно памяти, чтобы на самом деле правильно регистрировать утечки в течение длительного периода. По крайней мере, не в стандартной ОЗУ - есть еще одна куча ОЗУ, доступная в другом месте, через пользовательские функции распределения.

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

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

Ответ 8

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

Ответ 9

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

Причина, по которой пользовательское распределение через время работы ускорителя может быть полезным при использовании ускорителей, заключается в следующем:

  • через пользовательское распределение, время выполнения ускорителя или драйвер уведомляется о блоке памяти
  • Кроме того, операционная система может удостовериться, что выделенный блок памяти заблокирован (некоторые называют эту закрепленную память), то есть подсистема виртуальной памяти операционной системы не может перемещать или удалять страницу внутри или из память
  • Если выполняются операции 1. и 2. и запрашивается передача данных между блоком памяти с блокировкой страницы и ускорителем, среда выполнения может напрямую обращаться к данным в основной памяти, так как она знает, где она находится, и она может быть уверенной в том, что система не перемещала/удаляла его.
  • это сохраняет одну копию памяти, которая будет происходить с памятью, которая была распределена по незащищенному пути: данные должны быть скопированы в основной памяти в область промежуточной блокировки страницы с помощью ускорителя, который может инициализировать передачу данных (через DMA)

Ответ 10

Одна существенная ситуация: при написании кода, который должен работать с границами модулей (EXE/DLL), важно, чтобы ваши распределения и удаления происходили только в одном модуле.

Где я столкнулся с этим, это была архитектура плагинов в Windows. Очень важно, чтобы, например, если вы передали std::string по границе DLL, что любые перераспределения строки происходят из кучи, откуда она возникла, НЕ куча в DLL, которая может быть различной *.

* Это сложнее, чем это на самом деле, как если бы вы динамически связывались с ЭЛТ, это могло бы работать в любом случае. Но если каждая DLL имеет статическую ссылку на CRT, вы направляетесь в мир боли, где phantom ошибки распределения постоянно происходят.

Ответ 11

Одним из примеров того, что я использовал, я работал с очень ограниченными встроенными системами. Допустим, у вас есть 2k RAM бесплатно, и ваша программа должна использовать часть этой памяти. Вам нужно хранить примерно 4-5 последовательностей где-то, что не в стеке, и, кроме того, вам нужно иметь очень точный доступ к тому, где эти вещи будут храниться, это ситуация, когда вам может понадобиться написать собственный распределитель. Реализации по умолчанию могут фрагментировать память, это может быть неприемлемо, если у вас недостаточно памяти и не может перезапустить вашу программу.

В одном проекте, над которым я работал, использовался AVR-GCC на некоторых низкопроизводительных чипах. Нам пришлось хранить 8 последовательностей переменной длины, но с известным максимумом. Стандартная библиотека стандартная библиотека управления памятью - это тонкая оболочка вокруг malloc/free, которая отслеживает, где размещать элементы, добавляя каждый выделенный блок памяти с указателем на то, чтобы просто пройти конец выделенной части памяти. При распределении новой части памяти стандартный распределитель должен пройти по каждой части памяти, чтобы найти следующий блок, доступный там, где будет задан требуемый размер памяти. На настольной платформе это будет очень быстро для этих нескольких элементов, но вы должны иметь в виду, что некоторые из этих микроконтроллеров очень медленны и примитивны в сравнении. Кроме того, проблема фрагментации памяти была серьезной проблемой, которая означала, что у нас действительно не было выбора, кроме как принять другой подход.

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

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

Ответ 12

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

Распределитель Boost:: Interprocess является хорошим примером. Однако, поскольку вы можете читать здесь, этого все не достаточно, чтобы все совместимые контейнеры STL-контейнеров (в связи с различными смещениями сопоставления в разных процессах, указатели могут "ломаться" ).

Ответ 13

Обязательная ссылка на Andrei Alexandrescu CppCon 2015 Обсуждение на распределителях:

https://www.youtube.com/watch?v=LIb3L4vKZ7U

Самое приятное, что просто их разработка заставляет задуматься о том, как вы их используете: -)

Ответ 14

Я лично использую Loki:: Allocator/SmallObject для оптимизации использования памяти для небольших объектов - он показывает хорошую эффективность и удовлетворяет производительность, если вам приходится работать с умеренным количеством действительно маленьких объектов (от 1 до 256 байтов). Это может быть до ~ 30 раз более эффективным, чем стандартное выделение С++ new/delete, если мы говорим о распределении умеренных объемов небольших объектов самых разных размеров. Кроме того, существует специальное решение VC, называемое QuickHeap, оно обеспечивает наилучшую производительность (выделять и освобождать операции, просто читать и записывать адрес блока, который выделяется/возвращается в кучу, соответственно, до 99. (9)% случаев - зависит от настроек и инициализации), но за счет значительных накладных расходов - для каждого нового блока памяти требуется два указателя на один размер и один дополнительный. Это самое быстрое решение для работы с огромными (10 000 ++) количествами объектов, которые создаются и удаляются, если вам не требуется большое разнообразие размеров объектов (он создает отдельный пул для каждого размера объекта, от 1 до 1023 байтов в текущей реализации, поэтому затраты на инициализацию могут умалить общий прирост производительности, но можно идти вперед и выделять/освобождать некоторые фиктивные объекты до того, как приложение войдет в фазу (-ы) производительности.

Проблема со стандартной реализацией С++ new/delete заключается в том, что она обычно просто оболочка для C malloc/free allocation, и она хорошо работает для больших блоков памяти, например 1024 байта. Он имеет заметные накладные расходы с точки зрения производительности, а иногда и дополнительную память, используемую для сопоставления. Таким образом, в большинстве случаев пользовательские распределители реализованы таким образом, чтобы максимизировать производительность и/или минимизировать объем дополнительной памяти, необходимый для выделения небольших (≤1024 байтов) объектов.

Ответ 15

В графическом симуляции я видел пользовательские распределители, используемые для

  • Ограничения выравнивания, которые std::allocator не поддерживали напрямую.
  • Сведение к минимуму фрагментации с использованием отдельных пулов для недолговечного (только этого кадра) и долгоживущих распределений.

Ответ 16

Когда-то я нашел это решение очень полезным для меня: Fast С++ 11 allocator для контейнеров STL. Он немного ускоряет контейнеры STL на VS2017 (~ 5x), а также на GCC (~ 7x). Это специализированный распределитель на основе пула памяти. Он может использоваться с контейнерами STL только благодаря механизму, о котором вы просите.