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

Обзор

Если вы не работаете со встроенными системами с очень ограниченными ресурсами, работающими под управлением ОСРВ или на «голом железе», вам почти наверняка потребуется динамическое выделение памяти для обработки ваших данных. В C ++ существует множество методов динамического выделения памяти, таких как использование операторов new и delete и их аналогов new [] и delete [ ], std :: allocator или malloc () языка C.

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

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

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

Схема памяти

Физическая и виртуальная память

Имейте в виду, что эта структура памяти представляет собой виртуальную структуру памяти для приложений пользовательского пространства. В таких системах, как Linux, физическая память в целом делится на пространство ядра и пространство пользователя, для приложений мы говорим о пространстве пользователя. Кроме того, каждому процессу в системе назначается виртуальная память, которая обычно больше, чем доступная. Например, в системе с 4 ГБ памяти каждый процесс предполагает, что у него есть все, что ему доступно.

Схема памяти C ++

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

Текст / код - это место, где хранятся наши инструкции кода, данные / BSS - это место, где хранятся наши глобальные данные (инициализированные / неинициализированные), а стек используется для стеков вызовов для управления вызовами функций и локальными переменными.

Как ОС управляет кучей?

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

В Linux мы можем выделять / освобождать память, регулируя Program Break, который является текущим пределом кучи. Приложения пользовательского пространства могут настраивать его с помощью системных вызовов brk () и sbrk (), включенных в unistd.h, подробности см. На странице руководства.

Такой способ управления памятью не рекомендуется, так как это чревато ошибками. Первый уровень абстракции - это библиотека распределения памяти, предоставляемая средой выполнения C, семейство malloc ().

C Распределение динамической памяти

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

  • malloc (): выделяет память с учетом ее размера
  • free (): освобождает ранее выделенную память
  • realloc (): изменяет размер ранее выделенной памяти
  • calloc (): выделяет память для массива объектов

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

int *ptr = (int *) malloc(sizeof(int));
free(ptr);

Семейство API-интерфейсов malloc () использует системные вызовы brk (), sbrk () и mmap () для управления памятью. Это первый уровень абстракции.

Как работает malloc ()?

Детали реализации могут варьироваться от компилятора к компилятору и от ОС к ОС, но здесь мы рассмотрим обзор управления памятью, выполняемого malloc ().

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

  • Размер
  • Статус распределения (используется / бесплатно)

Когда вы вызываете malloc (), realloc () или calloc (), происходит поиск свободной области в памяти, которая соответствует запрошенному вами размеру.

Например, на изображении выше свободные блоки показаны синим цветом. Если размер подходит или меньше, блоки будут использоваться повторно, в противном случае, если в системе все еще доступны свободные области в памяти, malloc () выделит новую память, вызвав системные вызовы brk (), sbrk () или mmap (). .

Если в системе нет доступной памяти, malloc () завершится ошибкой и вернет NULL.

Выделение памяти

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

  • First-fit: первые найденные подходящие блоки памяти
  • Next-fit: второй встреченный подходящий блок памяти
  • Оптимальная посадка: оптимальная посадка по размеру

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

Освобождение памяти

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

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

Если в настоящее время у нас есть небольшой свободный блок посередине, показанный на изображении ниже:

И освобождаем последний блок:

Они объединятся в более крупный блок:

Перераспределение памяти

Как вы, возможно, догадались, перераспределение памяти, то есть когда мы вызываем realloc (), просто выделяет память + копирует существующие данные во вновь выделенную область.

Фрагментация памяти

Фрагментация памяти - это состояние, при котором небольшие блоки памяти выделяются в памяти между большими блоками. Это условие приводит к тому, что система не может выделить память, даже если большие области могут быть нераспределены.

Изображение ниже иллюстрирует сценарий:

Мы выделяем 3 блока, 1 блок и 8 блоков:

Затем мы освобождаем 3 блока и 8 блоков:

Теперь мы хотим выделить 9 блоков, это не удается, хотя у нас всего 11 свободных блоков, но они фрагментированы.

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

Распределение динамической памяти C ++

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

Создать и удалить оператор

В C ++, когда мы хотим выделить память из свободного хранилища (или мы можем назвать это кучей), мы используем оператор new.

int *ptr = new int;

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

delete ptr;

Отличие от malloc () в языке программирования C состоит в том, что оператор new выполняет две функции:

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

Аналогичным образом оператор delete выполняет две функции:

  • Уничтожьте объект, вызвав его деструктор
  • Освободите память (возможно, вызвав free ())

Следующий код показывает это:

Для выделения памяти и построения массива объектов мы используем:

MyData *ptr = new MyData[3]{1, 2, 3};

а для уничтожения и освобождения мы используем:

delete[] ptr;

Если мы выделили блоки памяти и хотим создать только объект, мы можем использовать так называемое новое размещение.

typename std::aligned_storage<sizeof(MyData), alignof(MyData)>::type data;
MyData *ptr = new(&data) MyData(2);

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

std :: allocator

Как вы знаете, контейнеры STL, такие как std :: vector, std :: deque и т. Д., Динамически распределяют память внутри. Они позволяют вам использовать собственный объект-распределитель памяти, но, как это часто бывает, мы используем объект по умолчанию - std :: allocator.

Причина, по которой они не используют операторы new и delete, заключается в том, что они хотят выделять память и создавать объекты отдельно. Например, std :: vector динамически увеличивает свою память, удваивая ее текущий размер для оптимизации скорости, см. Другой мой пост для более подробной информации.



Мы можем подумать, что под капотом std :: allocator вызовет malloc (), хотя он может делать некоторые другие вещи, такие как предварительное выделение памяти для оптимизации.

Умные указатели

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

  • Утечка памяти, когда мы забываем освободить память
  • Сбой / неопределенное поведение, когда мы пытаемся освободить память, которая была освобождена или дважды освобождена
  • Сбой / неопределенное поведение, когда мы пытаемся получить доступ к блокам памяти, которые мы освободили

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

Они есть:

  • std :: unique_ptr
    объект, который управляет указателем другого типа, который не может быть скопирован (уникальный)
  • std :: shared_ptr
    похож на unique_ptr, но может делить владение, используя счетчик ссылок
  • std :: weak_ptr
    объект, не являющийся владельцем, он ссылается на указатель, но не владеет им

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

Другая библиотека управления памятью

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

Резюме

Существует несколько уровней абстракции о том, как мы можем выделить / освободить память в C ++. Их знание важно, потому что мы знаем не только, какой уровень мы должны использовать, но и какие проблемы могут возникнуть в наших приложениях. На изображении ниже показаны API на разных уровнях.