Пул памяти Memory Pool

Последнее обновление: 08.09.2023

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

Частое выделение-освобождение памяти несет дополнительные издержки, снижающие производительность. Кроме того, существует проблема дефрагментации. И если размер наибольшего свободного фрагмента меньше размера запрошенной памяти, то выделение памяти завершится неудачей, даже если совокупный размер всех свободных фрагментов больше размера запрошенной памяти. Потому что функция malloc() выделяет последовательный набор байтов. И в этом случае решением может быть применение пула памяти или паттерна Memory Pool

Пул памяти может:

  • располагаться в статической памяти

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

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

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

В простейшем и общем случае реализацию пула памяти можно представить следующим образом:

#define MAX_ELEMENTS 10    // максимальное количество элементов
#define ELEMENT_SIZE 255   // размер одного элемента

// структура, которая представляет один блок в пуле памяти
typedef struct
{
  bool occupied;
  char memory[ELEMENT_SIZE];
}PoolBlock;

static PoolBlock memory_pool[MAX_ELEMENTS]; // сам пул памяти

// если переданный размер не превышает максимальный, то возвращает указатель на свободный блок памяти 
// если свободных блоков нет или размер больше максимального, то возврашает NULL
void* take(size_t size)
{
  if(size <= ELEMENT_SIZE)   // если не больше размера одного блока
  {
    for(int i=0; i<MAX_ELEMENTS; i++)   // проходим по всем блокам
    {
      if(memory_pool[i].occupied == 0)    // если блок не занят
      {
        memory_pool[i].occupied = 1;      // указываем, что он занят
        return &(memory_pool[i].memory);    // возвращаем адрес на память этого блока
      }
    }
  }
  return NULL;
}

// освобождает указатель на блок памяти
void release(void* pointer)
{
  for(int i=0; i<MAX_ELEMENTS; i++)  // проходим по всем блокам
  {
    if(&(memory_pool[i].memory) == pointer) // если указатель указывает на память определенного блока
    {
      memory_pool[i].occupied = 0;    // указываем, что блок теперь свободен
      return;
    }
  }
}

В этом листинге пул расположен в статической памяти. Пул делится на блоки. Каждый блок представлен структурой PoolBlock. Каждый блок определяет поле occupied, которое указывает, свободен ли блок. Также в блоке определен массив байтов memory - собственно память блока. Размер памяти блока задается значением ELEMENT_SIZE. А общее количество блоков в пуле задается значением MAX_ELEMENTS.

Для управления пулом определены две функции. Функция take получает размер памяти, которую надо выделить. Если этот размер меньше или равен размеру одного блока, то проходим по всем блокам и смотрим, какой из них свободен. Если нашли свободный, то возвращаем указатель на массив memory блока памяти. В остальных случаях возвращаем NULL.

Функция release освобождает блок памяти. Она принимает указатель на блок памяти, находит этот блок и сбрасывает его поле occupied в 0, что указывает, что блок свободен.

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

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

Простейший пример использования пула:

#include <stdio.h>
 
#define MAX_ELEMENTS 20    // максимальное количество элементов
#define ELEMENT_SIZE 255   // размер одного элемента

// структура, которая представляет один блок в пуле памяти
typedef struct
{
  int occupied;
  char memory[ELEMENT_SIZE];
} PoolBlock;

static PoolBlock memory_pool[MAX_ELEMENTS]; // сам пул памяти

// если переданный размер не превышает максимальный, то возвращает указатель на свободный блок памяти 
// если свободных блоков нет или размер больше максимального, то возврашает NULL
void* take(size_t size)
{
  if(size <= ELEMENT_SIZE)   // если не больше размера одного блока
  {
    for(size_t i=0; i<MAX_ELEMENTS; i++)   // проходим по всем блокам
    {
      if(memory_pool[i].occupied == 0)    // если блок не занят
      {
        memory_pool[i].occupied = 1;      // указываем, что он занят
        return &(memory_pool[i].memory);    // возвращаем адрес на память этого блока
      }
    }
  }
  return NULL;
}

// освобождает указатель на блок памяти
void release(void* pointer)
{
  for(int i=0; i<MAX_ELEMENTS; i++)  // проходим по всем блокам
  {
    if(&(memory_pool[i].memory) == pointer) // если указатель указывает на память определенного блока
    {
      memory_pool[i].occupied = 0;    // указываем, что блок теперь свободен
      return;
    }
  }
}

int main(void)
{
    int bufSize = 16;
    char* buffer = take(bufSize);  // запрашиваем память размером 16 байт
    if(buffer) // если память выделена
    {
        printf("Enter name: ");
        fgets(buffer, bufSize, stdin);  // вводим в buffer строку с консоли размером bufSize
        
        printf("Your name: %s\n", buffer);
        release(buffer);    // освобождаем память
    }
    else
    {
        printf("Unable to allocate memory\n");
    }
}

В данном случае запрашиваем в пуле памяти блок размеров в 16 байт и затем вводим в этот блок строку с консоли.

Причем хотя память одного блока представляет массив char, но в реальности мы можем разместить там данные любого типа или набора типов, размер которого укладывается в размер блока. Например, поместим в пул значение типа unsigned:

int main(void)
{
    unsigned* age = take(sizeof(unsigned));
    if(age)
    {
        printf("Enter age: ");
        scanf("%u", age);  // вводим в age число unsigned
        
        printf("Your age: %u\n", *age);
        release(age);    // освобождаем память
    }
    else
    {
        printf("Unable to allocate memory\n");
    }
}

Здесь для краткости приведен только код функции main, так как остальной код остается тем же. И в данном случае выделяем память под одно значение типа unsigned, на которое указывает переменная-указатель age.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850