IT "Понятно"
Обучение для начинающих
c 8 лет и до бесконечности


Как реализовано управление памятью в Python?

Как реализовано управление памятью в Python?

Опубликовано: 05.03.2024

Давайте подробнее разберем вопрос управления памятью в Python.


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

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

В компьютере есть физические устройства для работы с данными: жесткие диски, плашки оперативной памяти и др. Хранить и обрабатывать данные они умеют только на своем «компьютерном» языке, так называемом байт-коде. В языках программирования реализованы специальные механизмы перевода написанного программистом кода в понятный компьютеру байт-код. Чаще всего мы слышим про такие механизмы, как: компилятор и интерпретатор.

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

Мы рассмотрим, как работают механизмы управления памятью в эталонной реализации Python — CPython. Для того чтобы понять то, как в Python работает управлению памятью, сначала нужно немного разобраться с CPython.

Возможно, вы слышали о том, что всё в Python — это объект, даже примитивные типы данных вроде int и str. И это действительно так на уровне реализации языка в CPython. Тут существует структура, которая называется PyObject, которой пользуются объекты, создаваемые в CPython.

Структура (struct) — это композитный тип данных, который способен группировать данные разных типов. Если сравнить это с объектно-ориентированным программированием, то структура похожа на класс, у которого есть атрибуты, но нет методов.

PyObject — предок всех объектов Python. Эта структура содержит всего два поля:

  • ob_refcnt — счётчик ссылок.
  • ob_type — указатель на тип объекта.

Счётчик ссылок используется для реализации механизма сборки мусора. Другое поле PyObject — это указатель на конкретный тип объекта (например — это может быть тип dict или int).

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

Управление памятью осуществляется автоматически с помощью механизма сборки мусора (Garbage collector).

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

Используется метод подсчета ссылок для отслеживания того, когда объект уже не нужен, и этот объект должен быть освобожден. Кроме того, Python также использует циклический сборщик мусора (Cycle detector), который может определить и удалить объекты, на которые ссылается другой объект, на который уже нет ссылок.

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

В Python также реализованы другие алгоритмы сборки мусора, такие как generational garbage collection, который разбивает объекты на несколько "поколений" и собирает мусор с различной частотой в зависимости от поколения, в котором они находятся, но reference counting является основой управления памятью в Python.

Организация доступной виртуальной памяти

Непосредственно с оперативной памятью взаимодействует распределитель сырой памяти (raw memory allocator). Поверх него работают аллокаторы, реализующие стратегии управления памятью, специфичные для отдельных типов объектов. Объекты разных типов — например, числа и строки — занимают разный объем, к ним применяются разные механизмы хранения и освобождения памяти. Аллокаторы стараются не занимать лишнюю память до тех пор, пока она не станет совершенно необходимой — этот момент определен стратегией распределения памяти CPython.

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

  • Арена — фрагмент памяти, расположенный в пределах непрерывного блока оперативной памяти объемом 256 Кб. Объекты размером более 256 Кб направляются в стандартный аллокатор C.
  • Пул — блок памяти внутри арены, занимающий 4 Кб, что соответствует одной странице виртуальной памяти. То есть одна арена включает до 256/4 = 64 пулов.
  • Блок — элемент пула размером от 16 до 512 байт. В пределах пула все блоки имеют одинаковый размер. Размер блока определяется тем, сколько байт требуется для представления конкретного объекта. Размеры блоков кратны 16 байт. То есть существует всего 512/16 = 32 классов (size class) блоков. То есть в одном пуле, в зависимости от класса, может находиться от 8 до 256 блоков.

Блок

Блок содержит не более одного объекта Python и находится в одном из трех состояний:

  • untouched — блок еще не использовался для хранения данных;
  • free — блок использовался механизмом памяти, но больше не содержит используемых программой данных;
  • allocated — блок хранит данные, необходимые для выполнения программы.

В пределах пула блоки free организованы в односвязный список с указателем freeblock. Если аллокатору для выделения памяти не хватит блоков списка freeblock, он задействует блоки untouched. Освобождение памяти означает всего лишь то, что аллокатор меняет статус блока с allocated на free и начинает отслеживать блок в списке freeblock.

Пул

Пул может находиться в одном из трех состояний:

  • used (занят)
  • full (заполнен)
  • empty(пуст)

Пустые пулы отличаются от занятых отсутствием блоков allocated и тем, что для них пока не определен size class. Пулы full полностью заполнены блоками allocated и недоступны для записи. Стоит освободиться любому из блоков заполненного пула — и он помечается как used.

Пулы одного типа и одного размера блоков организованы в двусвязные списки. Это позволяет алгоритму легко находить доступное пространство для блока заданного размера. Алгоритм проверяет список usedpools и размещает блок в доступном пуле. Если в usedpools нет ни одного подходящего пула для запроса, алгоритм использует пул из списка freepools, который отслеживает пулы в состоянии empty.

Арена

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

Информацию о текущем распределении памяти в аренах, пулах и блоках можно посмотреть, запустив функцию sys._debugmallocstats().

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

Глобальная блокировка интерпретатора (Global Interpreter Lock, GIL) — это решение распространённой проблемы, возникающей при работе с разделяемыми ресурсами компьютера наподобие памяти. Когда два потока пытаются одновременно модифицировать один и тот же ресурс, они могут друг с другом «столкнуться». В результате получится беспорядок и ни один из потоков не достигнет того, к чему стремился.

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


Вам могут быть интересны материалы:

Нужен ли программисту в России английский язык?

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

Библиотеки для разработки игр на Python

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

Профилактика компьютерной зависимости у детей и подростков

Дети и подростки не в состоянии справиться самостоятельно с интернет-зависимостью. На начальных этапах может оказать эффект помощь со стороны родителей и друзей. Не стоит переходить к «жестким» запретительным мерам, так как можно столкнуться с непониманием и противодействием.


Наш сайт использует куки.
Пользуясь сайтом вы соглашаетесь
на обработку персональных данных.
Согласиться и закрыть это окно - нажмите «ОК».
OK