Лекция 8

Управление памятью

Содержание

  • новые и старые new и delete
  • stl аллокаторы
  • распространненые модели управления памятью

Старые new и delete

T * ptr = new T();
delete ptr;

Как работает new?

  1. определяет размер необходимой памяти для объекта
  2. запрашивает у ядра операционной системы (ОС) память необходимого размера
  3. вызывает конструктор объекта в выделенной ОС памяти
  4. возвращает указатель на выделенную в пункте 2 память

Как работает delete?

  1. оператор delete в качестве аргумента получает указатель на память, которую необходимо освободить
  2. вызывает деструктор объекта
  3. возвращает память обратно ядру ОС (зависит от; возможен случай когда память возвращается аллокатору находящемуся вне ядра).

Новые new

А как работает вызывать конструктор объекта в выделенной ОС памяти мне?

Placement new

Оператор new как и многие другие может быть переопределен.

void* operator new(std::size_t count, void* ptr);
static byte static_data[10000];

// ...

Type* foo = new (static_data) Type(42, "hello", "world");

Placement new

В С++ определен, так называемый, placement new, который НЕ выделяет память, а только создает объект в области памяти, которая передана в качестве аргумента.

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

Новые new

Ок, а какие еще есть переопределения new?

nothrow new

Как известно, если опертатор new не смог выделить память, то он генерирует исключение std::bad_alloc.

Функция malloc не генерирует исключения, а возвращает NULL.

nothrow new

Иногда требуется, чтобы оператор new не генерировал исключение в случае ошибки, а возвращал невалидный адрес (nullptr). Для таких целей можно использовать переопределенный оператор new с параметром std::nothrow.

nothrow new

void* operator new(size_t size, const std::nothrow_t &nt);
Type* ptr = new(std::nothrow) Type(42, "hello", "world");
if(ptr != nullptr) {
  // using ptr
  // ...
}

Пользовательские new

Можно определять свои операторы new

void* operator new (size_t cnt, const std::string& s) {
  std::cout << s << std::endl;
  // просим у системы память размером cnt
  return ::operator new(cnt);
}

Type* ptr = new (std::string("some debug message")) Type();
// ...
delete ptr;

Операторы для классов

С++ позволяет переопределять методы new и delete для классов.

struct Type {
  static void* operator new(std::size_t size)	{
    void* p = ::operator new(size);
    std::cout << "Type::new(" << size << ") " << p << std::endl;
    return p;
  }

  static void operator delete(void* p) {
    std::cout << "Type::delete(" << p << ")" << std::endl;
    if (p == nullptr)
      return;
    ::operator delete(p);
  }
};
Type* ptr = new Type();
delete ptr;

Резюме

Переопределенные операторы new позволяют управлять памятью в вашем приложении на любом уровне и позволяют писать приложения под любые требования.

Но, к счастью (или сожалению), управлять памятью на таком уровне требуется не часто, только в очень специфичных условиях.

Модели управления памятью

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

struct Block {
  size_t size;
  bool avaible;
};

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

Модель основана на массиве блоков памяти.

Выделение памяти сравнимо с задачей нахождения первого свободного блока.

Освобождение памяти: задача “удаление” элемента из массива.

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

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

По мере выделения памяти появляются новые блоки. Размер блоков определяется размером объектов распологаемых в блоках.

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

При новом запросе на выделение памяти последовательно проверяются все доступные блоки необходимого размера.

Освобождение блока памяти приводит к очередному поиску предыдущего свободного блока и изменению его размера.

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

Главный минус такой модели – скорость выделения и освобождения памяти: O(N), где N - количество блоков.

Vector vs list

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

struct MCB {
  MCB* next;
};

Простая модель управления памятью

Модель основана на списке из блоков памяти одинакового размера.

Простая модель управления памятью

Выделение памяти: возвращаем первый элемент списка (а ля pop).

Простая модель управления памятью

Выделение памяти: возвращаем первый элемент списка (а ля pop).

Простая модель управления памятью

Освобождение памяти: добавляем в список участок памяти (а ля push).

Простая модель управления памятью

Освобождение памяти: добавляем в список участок памяти (а ля push).

Простая модель управления памятью

Главный плюс такой модели – константная скорость выделения и освобождения памяти.

Минус – фрагментация памяти. Участки памяти перемешиваются, расходуется памяти больше, чем необходимо под объект.

Основные проблемы управления памятью

  • фрагментация памяти
  • параллелизм и конкурентность

Борьба с фрагментацией памяти

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

Когда требуется выделить N байт, модель ищет экземпляр аллокатора c размером блока, больше или равным N.

Работа в условиях многопоточности

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

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

TCMalloc* от Google

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

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

* TC = Thread Caching

Резюме

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

Выбор модели необходимо осуществлять в зависимости от задачи, требований и других условий.

STL Allocator

В некоторых частях стандартной библиотеки языка С++ используются специальные объекты для выделения и освобождения памяти, которые называются аллокаторами.

STL Allocator

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

Где и как используются

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

Где и как используются

template <class T, class Alloc = allocator<T>>
class vector;

template <class T, class Alloc = allocator<T>>
class list;

template <class Key, class T,
          class Hash = hash<Key>, class Pred = equal_to<Key>,
          class Alloc = allocator<pair<const Key,T>> >
class unordered_map;

Как работают аллокаторы

  • allocate(size_t N) – выделяет память для N элементов (n * sizeof(T))
  • construct(void* p, Args&&... args) – инициализирует элемент по адресу p, используя аргументы args
  • destroy(void *p) – уничтожает элемент по адресу p
  • deallocate(void* p, size_t N) – освобождает память по адресу p в которой располагается N элементов.

Пользовательские аллокаторы

Начиная с С++11 для создания собственного аллокатора требуется определить только функции allocate и deallocate.

По умолчанию, функция construct использует placement new, а destroy явно вызовает деструктор.

Резюме

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

Так же аллокаторы могут быть полезны при профилировании вашего приложения.

Литература