Почему вашему приложению нужна куча (Heap) больше, чем стек (Stack)

13

Ваш компьютер обладает не просто оперативной памятью (RAM). Это хаотичный, фрагментированный и постоянно меняющийся ландшафт памяти, который процессор (CPU) притворяется бесконечным. Это и есть реальность динамических структур данных, работающих на современном оборудовании.

Типичная рабочая станция сегодня оснащена 16–64 мегабайтами физической оперативной памяти. Но не дайте этим небольшим цифрам себя обмануть. Благодаря технологии, называемой виртуальной памятью, система осуществляет обмен данными между этой оперативной памятью и жестким диском. Процессор видит иллюзию. Ему кажется, что он располагает 200–500 мегабайтами непрерывного пространства. Эта иллюзия работает. Код выполняется. Но когда операционной системе приходится обращаться к жесткому диску для загрузки страницы памяти, всё замедляется. Вы чувствуете это в виде задержек. Вы чувствуете это в повышении оборотов вентилятора. Несмотря на снижение производительности, виртуальная память — это недорогой способ «расширить» объем вашей оперативной памяти. Это необходимая компромиссная мера.

Предположим, у нас есть чистый лист. Общий объем памяти составляет 50 мегабайт. Не больше. Не меньше. Это наш бюджет.

Операционная система владеет этим 50-мегабайтным блоком. Она делит этот пирог. Неравномерно. Никогда не равномерно.

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

Затем идет стек.

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

Когда программа завершает работу, операционная система выгружает её. Код, глобальные переменные, пространство стека — всё это стирается. Эта память перерабатывается. Она готова для следующей программы.

Но вот в чем проблема. В любой данный момент примерно 50 процентов этого 50-мегабайтного пространства может быть неиспользуемым. Почему? Потому что стек содержит только то, что выполняется в данный момент. Код содержит только инструкции. Непереработанные фрагменты памяти разбросаны. Это дыры.

Операционная система собирает эти дыры вместе. Она называет этот объединенный пул кучей (heap).

Именно здесь происходит динамическое выделение памяти. Куча — это единственное место в памяти, где программа может запросить точный объем пространства ровно в тот момент, когда он нужен. Вы не объявляете массив из 1000 целых чисел во время компиляции. Вы не знаете, понадобятся ли вам 1000 или 100 000 элементов. Вы ждете. Вы запускаете код. Вы решаете, что вам нужно это пространство. Затем вы вызываете malloc.

malloc означает memory allocate (выделение памяти). Она захватывает блок из кучи. Она возвращает указатель. Вы используете этот указатель. Когда вы закончите, вы вызываете free. Блок возвращается в пул.

Это фундаментальное различие между статической и динамической памятью. Стек автоматический. Он привязан к области видимости. Куча — ручная. Она привязана к намерению.

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

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

Куча обеспечивает гибкость. Она позволяет создавать структуры данных, которые могут расти. Связные списки. Деревья. Графы. Ни одну из этих структур нельзя определить во время компиляции с фиксированным размером. Они должны создаваться на лету. Они должны жить в куче.

Идеальна ли куча? Нет. Она фрагментируется. По мере выделения и освобождения блоков разного размера куча превращается в головоломку с отсутствующими кусочками. Поиск непрерывного блока свободной памяти может становиться медленным. Именно поэтому malloc иногда может занимать заметное время. Ей приходится искать. Ей приходится объединять фрагменты. Ей приходится управлять этим хаосом.

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