Каковы обычные детали реализации за пулами памяти?

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

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

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

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

От этот вопрос о шаблонах проектирования пула памяти C ++ 11, Я прочел:

Если вы еще этого не сделали, ознакомьтесь с Boost.Pool.
Из документации Boost:

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

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

Был бы признателен за простой пример, который показывает, как использовать пулы памяти.

16

Решение

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

Пулы памяти — это просто память, которую вы заранее распределили (и обычно в больших блоках). Например, вы можете выделить 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();
}

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

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

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

22

Другие решения

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

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

Первый подход заключается в определении собственного класса, который инкапсулирует пул памяти и предоставляет пользовательские методы, которые реализуют семантику new а также delete, но взять память из предварительно выделенного пула. Помните, что этот пул — не более чем область памяти, выделенная с помощью new и имеет произвольный размер. Версия пула new/delete возврат соотв. взять указатели. Самая простая версия, вероятно, будет выглядеть как код 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(), В конце концов Маф Фосбург изобрел псевдооператор, который использовал приоритет оператора, чтобы избавиться от (*handle)->method() синтаксис: handle[0]->method(); Это называлось сильный оператор.

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

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

6

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

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

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

1
По вопросам рекламы [email protected]