Опубликовано: 05.03.2024
Давайте подробнее разберем вопрос управления памятью в Python.Начинающие программисты, занимающиеся разработкой простых программ, не задумываются об эффективном расходовании ресурсов компьютера пользователя. Тем более, что Python многое делает за нас и мы привыкли не заботиться об управлении памятью и о написании соответствующего кода. Но, как только мы переходим к разработке более серьезных проектов и решению высоконагруженных задач, -подготовить производительный код становится сложно без понимания взаимодействия интерпретатора Python с оперативной памятью компьютера. Давайте разберёмся в этом во всем по порядку.
В компьютере есть физические устройства для работы с данными: жесткие диски, плашки оперативной памяти и др. Хранить и обрабатывать данные они умеют только на своем «компьютерном» языке, так называемом байт-коде. В языках программирования реализованы специальные механизмы перевода написанного программистом кода в понятный компьютеру байт-код. Чаще всего мы слышим про такие механизмы, как: компилятор и интерпретатор.
Python — это интерпретируемый язык. Код, написанный на Python, преобразуется в набор инструкций, с которым удобно работать компьютеру. Эти инструкции интерпретируются виртуальной машиной, когда вы запускаете свою программу. Виртуальные машины похожи на обычные компьютеры, но они реализованы программными средствами. Если вы уже работали с Python, то скорее всего уже видели файлы с расширением .pyc или папку __pycache__? В них и находится тот самый байт-код, который интерпретируется виртуальной машиной.
Мы рассмотрим, как работают механизмы управления памятью в эталонной реализации Python — CPython. Для того чтобы понять то, как в Python работает управлению памятью, сначала нужно немного разобраться с CPython.
Возможно, вы слышали о том, что всё в Python — это объект, даже примитивные типы данных вроде int и str. И это действительно так на уровне реализации языка в CPython. Тут существует структура, которая называется PyObject, которой пользуются объекты, создаваемые в CPython.
Структура (struct) — это композитный тип данных, который способен группировать данные разных типов. Если сравнить это с объектно-ориентированным программированием, то структура похожа на класс, у которого есть атрибуты, но нет методов.
PyObject — предок всех объектов Python. Эта структура содержит всего два поля:
Счётчик ссылок используется для реализации механизма сборки мусора. Другое поле 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 Кб:
Блок содержит не более одного объекта Python и находится в одном из трех состояний:
В пределах пула блоки free организованы в односвязный список с указателем freeblock. Если аллокатору для выделения памяти не хватит блоков списка freeblock, он задействует блоки untouched. Освобождение памяти означает всего лишь то, что аллокатор меняет статус блока с allocated на free и начинает отслеживать блок в списке freeblock.
Пул может находиться в одном из трех состояний:
Пустые пулы отличаются от занятых отсутствием блоков allocated и тем, что для них пока не определен size class. Пулы full полностью заполнены блоками allocated и недоступны для записи. Стоит освободиться любому из блоков заполненного пула — и он помечается как used.
Пулы одного типа и одного размера блоков организованы в двусвязные списки. Это позволяет алгоритму легко находить доступное пространство для блока заданного размера. Алгоритм проверяет список usedpools и размещает блок в доступном пуле. Если в usedpools нет ни одного подходящего пула для запроса, алгоритм использует пул из списка freepools, который отслеживает пулы в состоянии empty.
Арены содержат пулы любых видов и организованы в двусвязный список usable_arenas. Список отсортирован по количеству доступных пустых пулов. Чем меньше в арене таких пулов, тем она ближе к началу списка. Для размещения новых данных выбирается область, наиболее заполненная данными.
Информацию о текущем распределении памяти в аренах, пулах и блоках можно посмотреть, запустив функцию sys._debugmallocstats().
Глобальная блокировка интерпретатора (Global Interpreter Lock, GIL) — это решение распространённой проблемы, возникающей при работе с разделяемыми ресурсами компьютера наподобие памяти. Когда два потока пытаются одновременно модифицировать один и тот же ресурс, они могут друг с другом «столкнуться». В результате получится беспорядок и ни один из потоков не достигнет того, к чему стремился.
Механизм GIL в Python достигает этой цели, блокируя весь интерпретатор. В результате ничто не может помешать работе текущего потока. И когда CPython занимается работой с памятью, он использует GIL для того, чтобы эта работа делалась бы безопасно и качественно.
Рубрики по темам:
Нужен ли программисту в России английский язык?
Хотя знание английского языка не обязательно для работы программистом, это может серьезно повлиять на его эффективность и возможности карьерного роста.
Библиотеки для разработки игр на Python
Python можно использовать не только в работе с данными, но и для создания игр — например, платформеров, шутеров или песочниц. Я собрал для Вас несколько простых инструментов для геймдева.
Профилактика компьютерной зависимости у детей и подростков
Дети и подростки не в состоянии справиться самостоятельно с интернет-зависимостью. На начальных этапах может оказать эффект помощь со стороны родителей и друзей. Не стоит переходить к «жестким» запретительным мерам, так как можно столкнуться с непониманием и противодействием.