Убедительные примеры пользовательских C ++-распределителей?

StackOverflow https://stackoverflow.com/questions/826569

Вопрос

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

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

Это было полезно?

Решение

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

std::vector<T>

до

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

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

Другие советы

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

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

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

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

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

namespace mmap_allocator_namespace
{
        // See StackOverflow replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
        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. Не стесняйтесь использовать его для своих проектов.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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