Аллокаторы памяти

139
08 июля 2019, 07:20

Не так давно услышал о том, что существует способ управлять памятью самому, а не использовать, например, new и delete. Может кто-нибудь сможет осветить эту тему поподробнее, и привести какой-нибудь пример кода на С или С++, так как в интернете нашел мало информации на эту тему.

Answer 1

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

  1. Выделение памяти при помощи стандартных библиотечных функций обычно требует звонков в ядро, думаю все прекрасно понимают, что эта операция является не совсем быстрой.
  2. Нет никакого способа узнать, где та память, которую вернёт вам malloc или new, будет находиться по отношению к другим областям памяти в вашем приложении, в связи с этим получается значительная фрагментация памяти, да и к тому же будет больше промахов в кеше.

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

struct linear_allocator_t
{
    void* base_pointer;
    size_t size;
    size_t offset;
};
typedef struct linear_allocator_t linear_allocator_t;

Создаем структуру, которая имеет несколько полей: base_pointer - указатель на выделенный участок памяти при помощи стандартной библиотечной функций, size - размер выделенной памяти, offset - смещение, относительно последнего выделения памяти, уже нашим собственным аллокатором.

int la_init(linear_allocator_t* allocator, size_t memory_size)
{
    if (memory_size == 0)
        return 0;
    allocator->offset = 0;
    allocator->size = memory_size;
    allocator->base_pointer = malloc(memory_size);
    return allocator->base_pointer != NULL;
} 
void la_allocate(linear_allocator_t* allocator, size_t allocated_size)
{
    // Проверяем на достаточное количество памяти для выделения
    if (allocator->offset + allocated_size > allocator->size)
        return NULL;
    // Проверяем на не нулевой размер и на выравнивание
    if ( (allocated_size == 0) || (allocator->size % allocated_size != 0) )
        return NULL;
    size_t allocated_pointer = (size_t) allocator->base_pointer + allocator->offset;
    allocator->offset += allocated_size;
    return (void*) allocated_pointer;
}
// Данный аллокатор не поддерживает данную операцию(функция добавлена в качестве пояснения о существовании текущей операции)
void la_free(linear_allocator_t* allocator, void* pointer)
{
    assert(false && "Current allocator does not support current method. Use la_reset()");
}
void la_reset(linear_allocator_t* allocator)
{
    free(allocator->base_pointer);
}

Ну и напоследок само использование нашего аллокатора в действии:

// Тестовая структура, только для проверки
struct vector_4d_t
{
    double x, y, z, w;
};
typedef struct vector_4d_t vector_4d_t;
int main()
{
    // В данном примере фрагментация отсутствует
    linear_allocator_t allocator;
    la_init(&allocator, sizeof(vector_4d_t) * 100);
    for (int32_t i = 0; i < 100; i++)
    {
        vector_4d_t* vector = (vector_4d_t*) la_allocate(&allocator, sizeof(vector_4d_t));
        vector->x = vector->y = vector->z = vector->w = i;
    }
    la_reset(&allocator);
    return EXIT_SUCCESS;
}

Надеюсь, что мой ответ оказался вам полезным!

Answer 2

Функциональность оператора new фактически сводится к

  1. Вызову функции выделения "сырой" памяти требуемого размера
  2. Инициализации объекта в этой "сырой" памяти

Никто вам не запрещает выполнять эти шаги самостоятельно: выделять "сырую" память любым удобным для вас способом, а затем инициализировать объект в этой памяти при помощи placement-new

// Создание объекта типа `T`
void *raw = my_malloc(sizeof(T)); // <- любой способ выделения памяти
T *pt = new(raw) T(параметры);    // <- инициализация объекта

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

Удаление объекта повторяет функциональность оператора delete, т.е. делается через синтаксис вызова псевдо-деструктора и вашу же функцию освобождения "сырой" памяти

pt->~T();
my_free(pt);

Вот и все.

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

READ ALSO
Получение элемента из QTableWidget

Получение элемента из QTableWidget

QTableWidgetУсловие: Создать квадратную матрицуМатрица должна содержать слова из 4х букв английского алфавита

160
Присваивание массива

Присваивание массива

Вопрос в следующем: если присваивать в цикле:

134
создание сервиса с помощью Java SPI

создание сервиса с помощью Java SPI

Необходимо сделать расширяемое за счет плагинов приложениеДля создания подобной системы проделал следующие шаги:

111