Убедительные примеры пользовательских C ++-распределителей? - PullRequest
162 голосов
/ 05 мая 2009

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

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

Ответы [ 16 ]

107 голосов
/ 05 мая 2009

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

std::vector<T>

до

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

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

76 голосов
/ 06 мая 2009

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

EASTL - Стандартная библиотека шаблонов электронных искусств

59 голосов
/ 21 сентября 2012

Я работаю над 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. Не стесняйтесь использовать его для своих проектов.

24 голосов
/ 06 мая 2009

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

18 голосов
/ 05 мая 2009

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

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

6 голосов
/ 29 октября 2014

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

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

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

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

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

5 голосов
/ 06 мая 2009

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

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

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

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

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

4 голосов
/ 27 мая 2011

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

3 голосов
/ 11 июля 2015

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

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

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

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

...