Python
OTUS. Онлайн-образование corporate blog

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

Original author: https://realpython.com/team/avantol/
Translation
Всем привет! Вот и закончились длинные мартовские выходные. Первую послепраздничную публикацию мы хотим посвятить полюбившемуся многим курсу — «Разработчик Python», который стартует менее, чем через 2 недели. Поехали.

Содержание

  1. Память – пустая книга
  2. Управление памятью: от оборудования к программному обеспечению
  3. Базовая реализация Python
  4. Концепция глобальной блокировки интерпретатора (Global Interpreter Lock, GIL)
  5. Сборщик мусора
  6. Управление памятью в CPython:
    • Пулы
    • Блоки
    • Арены
  7. Заключение



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

Прочитав эту статью, вы:

  • Узнаете больше о низкоуровневых операциях, особенно касательно памяти.
  • Поймете, как Python абстрагирует низкоуровневые операции.
  • Узнаете об алгоритмах управления памятью в Python.

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

Память – пустая книга

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

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

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

Управление памятью: от оборудования к программному обеспечению

Управление памятью – это процесс, в котором программные приложения считывают и записывают данные. Менеджер памяти определяет куда поместить данные программы. Поскольку количество памяти конечно, как и количество страниц в книге, соответственно, менеджеру нужно находить свободное место, чтобы предоставить его в пользование приложению. Этот процесс называется «выделение памяти» (memory allocation).

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

Один из основных уровней, лежащих над оборудованием (RAM, жесткий диск и т.п.) – это операционная система. Она управляет запросами на чтение и запись в память.
Над операционной системой есть прикладной уровень, на котором есть одна из реализаций Python (зашитая в вашу ОС или загруженная с сайта python.org). Управление памятью для кода на этом языке программирования регулируется специальными средствами Python. Алгоритмы и структуры, которые Python использует для управления памятью – это основная тема данной статьи.

Базовая реализация Python

Базовая реализация Python, или же «чистый Python» — это CPython, написанный на языке С.
Я очень удивился, когда впервые об этом услышал. Как один язык может быть написан на другом языке?! Ну, не в буквальном смысле, конечно, но идея примерно такая.

Язык Python описан в специальном справочном руководстве на английском языке. Однако само по себе это руководство не сильно полезно. Вам все еще нужно средство для интерпретации кода, написанного по правилам справочника.

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

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


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

Вы когда-нибудь видели файлы с расширением .pyc или папку __pycache__? Это тот самый байткод, который интерпретируется виртуальной машиной.
Важно понимать, что есть другие реализации помимо CPython, например IronPython, который компилируется и запускается в Microsoft Common Language Runtime (CLR). Jython компилируется в байткод Java, чтобы запускаться на виртуальной машине Java. А еще есть PyPy о котором можно написать отдельную статью, поэтому я упомяну о нем лишь вскользь.

В этой статье мы сфокусируемся на управление памятью с помощью средств CPython.
Внимание: Версии Python обновляются и в будущем может всякое случиться. На время написания статьи последней версией был Python 3.7.

Хорошо, мы имеем CPython, написанный на С, который интерпретирует байткод Python. Каким образом это соотносится с управлением памятью? Начнем с того, что алгоритмы и структуры для управления памятью существуют в коде CPython, на С. Чтобы понять эти принципы в Python, необходимо базовое понимание CPython.

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

Должно быть, вы слышали, что все в Python – это объект, даже типы, такие как int и str, например. Это справедливо на уровне реализации CPython. Существует такая структура, которая называется PyObject, которую использует каждый объект в CPython.

Заметка: Структурой в С называется пользовательский тип данных, который в себе группирует различные типы данных. Можно провести аналогию с объектно-ориентированными языками и сказать, что структура – это класс с атрибутами, но без методов.

PyObject – это прародитель всех объектов в Python, содержащий всего две вещи:

  • ob_refcnt: счетчик ссылок;
  • ob_type: указатель на другой тип.

Счетчик ссылок необходим для сбора мусора. Еще мы имеем указатель на конкретный тип объекта. Тип объекта – это всего лишь другая структура, которая описывает объекты в Python (такие как dict или int).

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

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

Глобальная блокировка интерпретатора (GIL)

GIL – это решение общей проблемы разделения памяти между такими объединенными ресурсами, как память компьютера. Когда два потока пытаются изменять один и тот же ресурс одновременно, они наступают друг другу на пятки. В результате в памяти образуется полнейший бардак и ни один процесс не закончит свою работу с желаемым результатом.

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

Одним из решений этой проблемы как раз-таки и является GIL, который блокирует интерпретатор на то время, пока поток взаимодействует с выделенным ресурсом, таким образом позволяя одному и только одному потоку писать в выделенную область памяти. Когда CPython распределяет память, он использует GIL чтобы убедиться в том, что делает это правильно.
У этого подхода есть как множество плюсов, так и множество минусов, поэтому GIL вызывает распри в Python сообществе. Чтобы узнать больше о GIL, я советую прочитать следующую статью.

Сборщик мусора

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

Счетчик ссылок может увеличиться по нескольким причинам. Например, он увеличится, если вы одну переменную присвоите другой переменной.



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



В последнем примере счетчик ссылок увеличится, если вы включите объект в список.



Python позволяет узнать текущее значение счетчика ссылок с помощью модуля sys. Вы можете использовать sys.getrefcount(numbers), но помните, что вызов getrefcount() увеличит счётчик ссылок еще на единицу.

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

Но что значит «освободить память» и как другие объекты ее используют? Давайте погрузимся непосредственно в управление памятью в CPython.

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

В этой части мы погрузимся в архитектуру памяти CPython и алгоритмы, по которым она функционирует.

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

ОС-ориентированный менеджер виртуальной памяти выделяет определенную область памяти под процессы Python. На картинке темно-серые области – это пространство, которое занимает процесс Python.



Python использует часть памяти для внутреннего использования и необъектной памяти (non-object memory). Другая часть делится на хранилище объектов (ваши int, dict и т.п.) Сейчас я изъясняюсь очень простым языком, однако вы можете заглянуть прямо «под капот», то есть в исходный код CPython и посмотреть как это все происходит с практической точки зрения.

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

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

Комментарии в исходном коде определяют распределитель (allocator) как «быстрый распределитель памяти специального назначения, который работает подобно универсальной функции malloc». Соответственно, в языке С malloc используется для выделения памяти.

Теперь давайте взглянем на стратегию выделения памяти в CPython. Для начала поговорим о трех основных частях и о том, как они друг с другом соотносятся.

Арены (arena) – самые большие области памяти, которые занимают место до границ страниц в памяти. Граница страницы (разворота) – это крайняя точка непрерывного блока памяти фиксированной длины, используемого ОС. Python устанавливает границу страницы системы в 256 Кб.



Внутри арен находятся пулы (pool), которые считаются одной виртуальной страницей памяти (4 Кб). Они похожи на страницы в нашей аналогии. Пулы разделены на еще более маленькие фрагменты памяти – блоки (block).

Все блоки в пуле находят в одном «классе размера». Класс размера (size class) определяет размер блока, имея определенное количество запрашиваемых данных. Градация в таблице снизу взята прямо из комментариев в исходном коде:



Например, если необходимы 42 байта, то данные будут помещены в блок размером 48 байт.

Пулы

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

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

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

Список пустых пулов (freepools list) содержит, соответственно, все пулы в пустом состоянии. Но в какой момент они используются?

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

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

Блоки



Как видно из рисунка, пулы содержат указатели на свободные блоки памяти. В их работе присутствует небольшой нюанс. Согласно комментариям в исходном коде, распределитель «стремиться никогда не трогать какую-либо область памяти на любом из уровней (арена, пул, блок), пока она не понадобится».

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

  • Нетронутые: области памяти, которые не были распределены;
  • Свободные: области памяти, которые были распределены, но позже освобождены CPython, поскольку не содержали в себе актуальной информации;
  • Распределенные: области памяти, которые на данный момент времени содержат актуальную информацию.

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

Как только менеджер памяти освобождает блоки, эти блоки добавляются в начало списка свободных блоков. Фактический список может не содержать непрерывную последовательность блоков памяти, как на первом «удачном» рисунке.



Арены

Арены содержат в себе пулы. Арены в отличие от пулов не имеют явных разделений на состояния.

Они сами по себе организованы в двухсвязный список, который называется список используемых арен (usable_arenas). Этот список отсортирован по количеству свободных пулов. Чем меньше свободных пулов, тем ближе арена к началу списка.



Это означает, что наиболее полная арена будет выбрана для записи еще большего количества данных. Но почему именно так? Почему бы не записывать данные туда, где больше всего свободного места?

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

Арены не единственные области, которые могут быть полностью освобождены. Таким образом мы понимаем, что те арены, которые находятся в списке «ближе к пустому состоянию», должны быть освобождены. В таком случае, область памяти может быть действительно полностью освобождена, и соответственно общий объем памяти вашей программы на Python уменьшен.

Заключение

Управление памятью – это одна из самых важный частей в работе с компьютером. Python так или иначе производит практически все действия в скрытом режиме.

Из данной статьи вы узнали:

  • Что такое управление памятью и почему оно важно;
  • Что представляет из себя CPython, базовая реализация Python;
  • Как структуры данных и алгоритмы работают в управлении памятью CPython и хранят ваши данные.

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

Вот мы и узнали об управлении памяти в Python. Традиционно ждём ваши комментарии, а также приглашаем на день открытых дверей по курсу «Разработчик Python», который пройдет уже 13 марта
+16
6.1k 148
Comments 5