Помнить всё. Как работает память в Python
Арены, пулы и блоки. Если сочетание этих трех слов вам ни о чем не говорит, пора узнать, как устроена работа с памятью в Python. Обсудить Python многое делает за нас. Мы привыкли не заботиться об управлении памятью и о написании соответствующего кода. Пусть эти процессы и скрыты, но без их понимания трудно подготовить производительный код для высоконагруженных задач. В этой статье мы рассмотрим модель памяти Python и то, как интерпретатор Python взаимодействует с оперативной памятью компьютера. Python — это интерпретируемый язык программирования, поэтому перед запуском программы код на языке Python компилируется в машиночитаемые инструкции — байт-код. Инструкции байт-кода интерпретируются виртуальной машиной, определяемой реализацией языка, например, стандартной — CPython. Доклад Егора Овчаренко «Устройство CPython» поможет разобраться в стандартной реализации Python Оговоримся, что CPython не взаимодействует напрямую с регистрами и ячейками физической памяти — только с ее виртуальным представлением. В начале выполнения программы операционная система создает новый процесс и выделяет под него ресурсы. Выделенную виртуальную память интерпретатор использует для 1) собственной корректной работы, 2) стека вызываемых функций и их аргументов и 3) хранилища данных, представленного в виде кучи. В отличие от C/C++, мы не можем управлять состоянием кучи напрямую из Python. Функции низкоуровневой работы с памятью предоставляются Python/C API, но обычно интерпретатор просто обращается к хранилищу данных через диспетчер памяти Python (memory manager). Диспетчер памяти — своеобразный портье, который регистрирует и расселяет гостей отеля. Каждый постоялец получает ключ с номером комнаты, так что ни один из гостей не может заселиться не в свой номер. Две программы не могут одновременно записать переменную в одно место виртуальной памяти. Фактически за это отвечает даже не диспетчер задач, который ожидает гостей за регистрационной стойкой, а GIL — глобальная блокировка интерпретатора. GIL гарантирует: в один и тот же момент времени байт-код выполняется только одним потоком. Главное преимущество — безопасная работа с памятью, а основной недостаток в том, что многопоточное выполнение программ Python требует специфических решений. Очевидно, программа не сама выполняет сохранение и освобождение памяти — ведь мы не пишем соответствующих инструкций. Интерпретатор лишь запрашивает диспетчер памяти сделать это. А диспетчер уже делегирует работу, связанную с хранением данных, аллокаторам — распределителям памяти. Непосредственно с оперативной памятью взаимодействует распределитель сырой памяти (raw memory allocator). Поверх него работают аллокаторы, реализующие стратегии управления памятью, специфичные для отдельных типов объектов. Объекты разных типов — например, числа и строки — занимают разный объем, к ним применяются разные механизмы хранения и освобождения памяти. Аллокаторы стараются не занимать лишнюю память до тех пор, пока она не станет совершенно необходимой — этот момент определен стратегией распределения памяти CPython. Python использует динамическую стратегию, то есть распределение памяти выполняется во время выполнения программы. Виртуальная память Python представляет иерархическую структуру, оптимизированную под объекты Python размером менее 256 Кб: Схематическое представление структуры виртуально памяти Python: арены состоят из пулов, пулы составлены из блоков Блок содержит не более одного объекта Python и находится в одном из трех состояний: В пределах пула блоки Пул может находиться в одном из трех состояний: Пулы одного типа и одного размера блоков организованы в двусвязные списки. Это позволяет алгоритму легко находить доступное пространство для блока заданного размера. Алгоритм проверяет список Арены содержат пулы любых видов и организованы в двусвязный список Информацию о текущем распределении памяти в аренах, пулах и блоках можно посмотреть, запустив функцию Чтобы не произошло утечки памяти, диспетчер памяти должен отследить, что вся выделенная память освободится после завершения работы программы. То есть при завершении программы CPython дает задание очистить все арены. Именно количество используемых арен определяет объем оперативной памяти, занимаемой программой на Python — если в арене все пулы в состоянии Для освобождения памяти используются два механизма: счетчик ссылок и сборщик мусора. Всё в Python является объектами, а прародителем всех типов объектов в реализации CPython является PyObject. От него наследуются все остальные типы. В PyObject определены счетчик ссылок и указатель на фактический тип объекта. Счетчик ссылок увеличивается на единицу, когда мы создаем что-то, что обращается к объекту, например, сохраняем объект в новой переменной. И наоборот, счетчик уменьшается на единицу, когда мы перестаем ссылаться на объект. Счетчик ссылок любого объекта можно проверить с помощью Однако счетчик ссылок неспособен отследить ситуации с циклическими ссылками. К примеру, возможна ситуация, когда два объекта ссылаются друг на друга, но оба уже не используются программой. Для борьбы с такими зависимостями используется сборщик мусора ( Если счетчик ссылок является свойством объекта, то сборщик мусора — механизм, который запускается на основе эвристик. Задача этих эвристик — снизить частоту и объем очищаемых данных. Основная стратегия заключается в разделении объектов на поколения: чем больше сборок мусора пережил объект, тем он значимее для выполнения работы программы. Сборщик мусора имеет интерфейс в виде модуля gc. Сохранение и освобождение блоков памяти требует времени и вычислительных ресурсов. Чем меньше блоков задействовано, тем выше скорость работы программы. Позволим себе дать несколько советов, касающихся экономной работы с памятью: На ранних этапах работы с кодом Python можно вполне обойтись стандартными средствами Python. Но по мере разрастания кодовой базы и приближения к коммерческому использованию кода производительность становится одним из ключевых факторов. Многие задачи, связанные с производительностью Python, лежат в области понимания устройства инструмента и конкретной реализации языка. Не пожалейте времени на изучение исходного кода CPython, находящегося в свободном доступе в репозитории на GitHub.Диспетчер памяти: «командовать парадом буду я»
Организация доступной виртуальной памяти
Блок
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()
:
>>> import sys >>> sys._debugmallocstats() Small block threshold = 512, in 32 size classes. class size num pools blocks in use avail blocks ----- ---- --------- ------------- ------------ 0 16 1 212 41 1 32 6 629 127 2 48 28 2317 35 3 64 118 7386 48 4 80 104 5164 36 5 96 17 696 18 6 112 11 372 24 7 128 7 212 5 8 144 37 1014 22 9 160 7 174 1 10 176 64 1426 46 11 192 4 66 18 12 208 3 42 15 13 224 11 181 17 14 240 5 65 15 15 256 3 35 10 16 272 2 23 5 17 288 3 28 14 18 304 15 184 11 19 320 2 17 7 20 336 2 14 10 21 352 2 13 9 22 368 2 15 7 23 384 1 9 1 24 400 3 23 7 25 416 4 31 5 26 432 6 42 12 27 448 4 28 8 28 464 4 26 6 29 480 4 27 5 30 496 5 33 7 31 512 6 37 5 # arenas allocated total = 10 # arenas reclaimed = 2 # arenas highwater mark = 8 # arenas allocated current = 8 8 arenas * 262144 bytes/arena = 2,097,152
empty
, СPython делает запрос на освобождение этого участка виртуальной памяти. Но уже понятно: чтобы пулы стали empty
, все их блоки должны быть free
или untouched
. Получается, нужно понять, как CPython освобождает память.Освобождение памяти: счетчик ссылок, сборщик мусора
sys.getrefcount()
. Учтите, что передача объекта в getrefcount()
увеличивает счетчик ссылок на 1, так как сам вызов метода создает еще одну ссылку. Когда счетчик уменьшается до нуля, происходит вызов аллокатора для освобождения соответствующих блоков памяти.
>>> import sys >>> a = {} >>> b = a >>> sys.getrefcount(a) 3 >>> del b >>> sys.getrefcount(a) 2
garbage collector
).Заключение
.join()
, .format()
или f-строки.
- 17 views
- 0 Comment