Время прочтения: 5 мин.

О том, как устроена память мы рассказывали в статье — Память в Python

Обозначим проблему

Процесс останавливается на свободном пуле. Кода для сопоставления свободных пулов с Аренами нет и, следовательно, вернуть свободную Арену в ОС мы не можем.

Для этого должен быть список, специально для пулов определенной арены, как только пул освобождается, он добавляется в этот список. Как только все пулы арены в этом списке — вся арена должна вернуться в ОС.

Для того, чтобы связать пул с ареной, с которой он был выделен, в заголовок пула был включен индекс арены.  freepools был изменен на partially_allocated_arenas. Этот список содержит арены, которые имеют доступные пулы (см. Рис ниже).

Теперь выделение памяти немного изменилось, как только нужен свободный пул, вместо того, чтобы взять его из freepools, он будет взят из первого элемента списка partially_allocated_arenas.

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

Таким образом, если пул полностью свободен, то он помещается в список свободных пулов своей арены. Связанный список partially_allocated_arenas – это список, содержащий арены, имеющие свободные пулы. Если арена полностью свободна, то она удаляется из списка и возвращается в ОС по вызову free().

Важное замечание, список partially_allocated_arenas сортируется таким образом, чтобы наиболее пустые арены, находились в конце, это дает шанс аренам, которые почти пусты, «покинуть игру»!

Последствия

Эти изменения не повлияют на корректность программ, это только улучшит их производительность, а версия распределителя способна возвращать память в операционную систему. Это должно сильно оптимизировать программы, имеющие скачкообразное потребление памяти. Есть и недостатки, например, появляются дополнительные расходы на отслеживание блоков (из какой арены они пришли). Если программа циклически требует память, то это дополнительные расходы на вызов malloc() (для выделения памяти) и free() (для освобождения памяти). До этого этих расходов не существовало, так как  Python удерживал столько памяти сколько надо программе в ее максимальных точках потребления. Но эти дополнительные расходы должны быть незначительными.

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

Пример применения

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

В идеальном случае, нам придется выделить около 30 Мб, затем 500Мб, освободить память до 30 Мб и снова занять 500Мб. Исходный распределитель поддерживает память всегда на максимальном значении около 500 Мб, новый же распределитель пытается следовать идеальному поведению. В нашем случае максимальный объем памяти используется практически сразу и это не очень эффективно, однако, если промежутки между скачками памяти будут длительными, данный распределитель окажется крайне эффективным.

Пример программы, которая выделяет и освобождает большие объемы памяти
Поведение распределителя памяти для примера программы

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

Несколько объектов, которые не используют распределитель pymalloc. Наиболее важными из них являются целые числа и  float. Эти типы данных поддерживают свой собственный список свободных объектов, чтобы более эффективно использовать пространство и время для этих очень распространенных объектов. Однако текущая реализация может привести к той же проблеме, которую пытались решить изменения, описанные в этой статье.

Эти два типа объектов выделяют свои собственные блоки памяти, которые выделяются вместе с системой malloc(). Эти блоки используются в качестве массивов целых чисел и float объектов. Затем эти объекты связываются в простой свободный список. Когда требуется объект, он берется из списка или выделяется новый блок. Когда объект освобождается, он возвращается в список свободных объектов.

Эта схема очень проста и очень быстра, но она демонстрирует существенную проблему: память, которая выделяется целым числам, никогда не может быть использована для чего-либо другого. Это означает, что если вы пишете программу, которая выполняется и выделяет 1 000 000 целых чисел, потом освобождает их и выделяет 1 000 000 float, Python будет удерживать достаточно памяти для 2 000 000 числовых объектов (Только для версии 2.х).

 Выводы

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