Как работают пулы памяти?

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

Все, что я знаю об этом, это то, что "пулы памяти, также называемые распределением блоков фиксированного размера" , как сообщает Wikipedia, и я могу использовать эти куски для выделения памяти для моих объектов >

Существуют ли какие-либо стандартные спецификации пулов памяти?

Я хотел бы знать, как это работает в куче, как это можно реализовать и как это можно использовать?

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

ИЗМЕНИТЬ

Что такое пул?

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

from этот вопрос

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

Ответ 1

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

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

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

class MemoryPool
{
public:
    MemoryPool(): ptr(mem) 
    {
    }

    void* allocate(int mem_size)
    {
        assert((ptr + mem_size) <= (mem + sizeof mem) && "Pool exhausted!");
        void* mem = ptr;
        ptr += mem_size;
        return mem;
    }

private:
    MemoryPool(const MemoryPool&);
    MemoryPool& operator=(const MemoryPool&);   
    char mem[4096];
    char* ptr;
};

...
{
    MemoryPool pool;

    // Allocate an instance of `Foo` into a chunk returned by the memory pool.
    Foo* foo = new(pool.allocate(sizeof(Foo))) Foo;
    ...
    // Invoke the dtor manually since we used placement new.
    foo->~Foo();
}

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

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

Реализация распределителей - это то, что я бы рекомендовал многим разработчикам C и С++, чтобы как-то созвучно тому, как управление памятью работает немного лучше. Это может сделать вас немного более осведомленным о том, как запрашиваемая память соединяется с используемыми ими структурами данных, а также открывает совершенно новую дверь возможностей оптимизации без использования каких-либо новых структур данных. Он также может создавать структуры данных, такие как связанные списки, которые, как правило, не очень эффективны, гораздо более полезны и уменьшают соблазны сделать непрозрачные/абстрактные типы менее непрозрачными, чтобы избежать накладных расходов кучи. Тем не менее, может быть начальное волнение, которое может хотеть сделать вас обувными пользовательскими распределителями для всего, только для того, чтобы потом сожалеть о дополнительной нагрузке (особенно если вы в своем волнении забываете о проблемах, таких как безопасность потока и выравнивание). Здесь стоит легко. Как и при любой микро-оптимизации, он лучше всего применяется дискретно, оглядываясь назад и с профайлером.

Ответ 2

Основная концепция пула памяти заключается в том, чтобы выделить большую часть памяти для вашего приложения, а позже вместо обычного new запросить память из O/S, вы вернете кусок ранее вместо этого выделяется память.

Чтобы выполнить эту работу, вам необходимо самостоятельно управлять использованием памяти и не полагаться на O/S; т.е. вам нужно будет реализовать свои собственные версии new и delete и использовать исходные версии только при распределении, освобождении или потенциальном изменении размера собственного пула памяти.

Первый подход заключался бы в определении одного собственного класса, который инкапсулирует пул памяти и предоставляет настраиваемые методы, реализующие семантику new и delete, но беря память из предварительно выделенного пула. Помните, что этот пул - это не что иное, как область памяти, которая была выделена с помощью new и имеет произвольный размер. Версия пула new/delete return resp. возьмите указатели. Простейшая версия, вероятно, будет выглядеть как C-код:

void *MyPool::malloc(const size_t &size)
void MyPool::free(void *ptr)

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

template <typename T>
T *MyClass::malloc();

template <typename T>
void MyClass::free(T *ptr);

Обратите внимание, что благодаря аргументам шаблона аргумент size_t size может быть опущен, поскольку компилятор позволяет вам вызвать sizeof(T) в malloc().

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

Способ исправления этого ограничения состоит в том, чтобы возвращать указатели указателям, т.е. возвращать T** вместо просто T*. Это позволяет вам изменять основной указатель, в то время как пользовательская часть остается неизменной. Кстати, это было сделано для NeXT O/S, где он назывался "ручкой". Чтобы получить доступ к содержимому дескриптора, нужно было позвонить (*handle)->method() или (**handle).method(). В конце концов, Maf Vosburg изобрел псевдооператор, который использовал приоритет оператора, чтобы избавиться от синтаксиса (*handle)->method(): handle[0]->method(); Он назывался sprong operator.

Преимущества этой операции: во-первых, вы избегаете накладных расходов на типичный вызов new и delete, а во-вторых, ваш пул памяти гарантирует, что ваш приёмный сегмент памяти будет использоваться вашим приложением, это позволяет избежать фрагментации памяти и, следовательно, увеличивает удары кэша ЦП.

Итак, в основном, пул памяти дает вам ускорение, которое вы получаете с недостатком потенциально более сложного кода приложения. Но опять же, есть некоторые реализации пулов памяти, которые доказаны и могут быть просто использованы, например boost:: pool.

Ответ 3

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

Другими словами, вместо вызовов на new/malloc и delete/free, вызовите свои самоопределяемые функции распределителя/деаллокатора.

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