Что такое фрагментация кучи (heap fragmentation) в C++?

Ответ

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

Основные причины в C++:

  • Частые и разноразмерные вызовы new/delete или malloc/free.
  • Неоптимальная стратегия работы аллокатора стандартной библиотеки.
  • Длительная работа программы с "рваным" паттерном выделения/освобождения.

Прямой пример на C++:

int* block1 = new int[100]; // Выделен блок A
int* block2 = new int[200]; // Выделен блок B
int* block3 = new int[50];  // Выделен блок C

delete[] block1; // Освобождается блок A
// Теперь в куче есть свободный блок на 100 int, но он расположен между занятыми блоками B и C.
// Попытка выделить блок на 150 int может завершиться неудачей (std::bad_alloc),
// несмотря на то, что суммарно свободно 100 + 50 (если освободить C) = 150 элементов.
int* block4 = new int[150]; // Может вызвать ошибку из-за фрагментации.

Последствия:

  • Сбои выделения памяти (std::bad_alloc) при, казалось бы, достаточном свободном месте.
  • Увеличение времени на поиск подходящего блока аллокатором.
  • Рост общего потребления памяти (оверхеда) из-за служебных структур.

Способы борьбы в C++:

  1. Использование специализированных аллокаторов: std::pmr::memory_resource (C++17) с пулами (std::pmr::synchronized_pool_resource).
  2. Выделение памяти крупными "страницами" с последующим ручным управлением внутри приложения.
  3. Минимизация количества динамических выделений: использование контейнеров с reserve(), стековых объектов, std::make_shared (который часто выделяет блок для объекта и контрольного блока вместе).
  4. Использование компактных структур данных, избегающих указателей на множество мелких объектов.

Ответ 18+ 🔞

Да ты послушай, что за дичь творится с этой кучей! Представь себе огромный склад, где вроде бы места дохуя, но всё заставлено коробками вразнобой. И ты такой: «Мне бы одну длинную полку на 150 коробок!» А тебе в ответ: «Не, братан, у нас тут свободно — вот кусочек на 100, а вот через два прохода ещё на 50, а между ними хрен пролезет, всё занято». Вот это и есть фрагментация, ёпта. В памяти — та же история.

Откуда эта хрень вообще берётся в C++?

  • Ты как сумасшедший кидаешь new и delete на объекты разного размера — аллокатор с ума сходит, всё в клочья рвёт.
  • Сам аллокатор стандартной библиотеки иногда такой хитрожопый, что только усугубляет.
  • Программа поработала пару суток, и куча превратилась в лоскутное одеяло — сплошные дырки, но все мелкие.

Смотри, как это в коде выглядит, чтоб совсем понятно было:

int* block1 = new int[100]; // Отхватили здоровенный кусок A
int* block2 = new int[200]; // Рядом ещё больший кусок B
int* block3 = new int[50];  // И маленький C впритык

delete[] block1; // Выкинули A, образовалась дыра
// Теперь свободное место есть — 100 int. Но оно зажато между B и C, как селёдка в бочке.
// Попробуй вот выделить 150 int подряд. Не выйдет, блядь! Хотя если бы C тоже освободить,
// то в сумме было бы 150. Но они же не рядом!
int* block4 = new int[150]; // Щёлк — std::bad_alloc, и ты охуел.

И чем это всё грозит?

  • «Памяти нет», хотя по факту она есть, просто в виде конфетти. Прога падает с bad_alloc, а ты чешешь репу.
  • Аллокатор начинает ебашить как проклятый, чтобы найти хоть какой-то подходящий кусок, и всё тормозит.
  • Памяти жрётся овердохуища из-за всякой служебной лабуды, которая пытается эту кашу упорядочить.

Что делать, чтобы не сойти с ума?

  1. Взять умный аллокатор. В C++17 есть std::pmr::memory_resource — это, блядь, спасение. Кинул ему synchronized_pool_resource, и он сам следит, чтобы куча не расползалась.
  2. Выделять память крупными пластами, как оптовик. Взял один здоровенный блок и внутри уже сам раздаёшь куски — фрагментации не будет, потому что все дырки в твоих руках.
  3. Меньше дёргать new. Используй reserve() у векторов, чтоб не перевыделять по сто раз. Что можно на стеке — держи на стеке. std::make_shared часто умнее, чем ручное создание shared_ptr — он одним махом и объект, и счётчик выделяет.
  4. Не городить структуры из миллиона мелких указателей. Это верный путь в ад. Компактные массивы, локализация данных — твои лучшие друзья. Чем меньше ты оставляешь аллокатору пространства для манёвра, тем меньше он тебя подставит.