Как узнать сколько памяти занимает объект python
Перейти к содержимому

Как узнать сколько памяти занимает объект python

  • автор:

Как узнать сколько памяти занимает объект python

In python, the usage of sys.getsizeof() can be done to find the storage size of a particular object that occupies some space in the memory. This function returns the size of the object in bytes. It takes at most two arguments i.e Object itself.

Note: Only the memory consumption directly attributed to the object is accounted for, not the memory consumption of objects it refers to.

Examples:

Here’s how we can interpret the actual output. Have a look at the table below:

Использование памяти в Python

image

Меня часто донимали размышление о том, насколько эффективно Python использует память по сравнению с другими языками программирования. Например, сколько памяти нужно, чтобы работать с 1 миллионом целых чисел? А с тем же количеством строк произвольной длины?
Как оказалось, в Python есть возможность получить необходимую информацию прямо из интерактивной консоли, не обращаясь к исходному коду на C (хотя, для верности, мы туда все таки заглянем).
Удовлетворив любопытство, мы залезем внутрь типов данных и узнаем, на что именно расходуется память.

Все примеры были сделаны в CPython версии 2.7.4 на 32 битной машине. В конце приведена таблица для потребности в памяти на 64 битной машине.

Необходимые инструменты
sys.getsizeof и метод __sizeof__()

Первый инструмент, который нам потребуется находится в стандартной библиотеки sys. Цитируем официальную документацию:

sys.getsizeof(объект[, значение_по_умолчанию])

Возвращает размер объекта в байтах.
Если указано значение по умолчанию, то оно вернется, если объект не предоставляет способа получить размер. В противном случае возникнет исключение TypeError.
Getsizeof() вызывает метод объекта __sizeof__ и добавляет размер дополнительной информации, которая хранится для сборщика мусора, если он используется.

Алгоритм работы getsizeof(), переписанной на Python, мог бы выглядеть следующем образом:

Где PyGC_Head — элемент двойного связанного списка, который используется сборщиком мусора для обнаружения кольцевых ссылок. В исходном коде он представлен следующей структурой:

Размер PyGC_Head будет равен 12 байт на 32 битной и 24 байта на 64 битной машине.

Попробуем вызвать getsizeof() в консоли и посмотрим, что получится:

За исключением магии с проверкой флагов, все очень просто.
Как видно из примера, int и float занимают 12 и 16 байт соответственно. Str занимает 21 байт и еще по одному байту на каждый символ содержимого. Пустой кортеж занимает 12 байт, и дополнительно 4 байта на каждый элемент. Для простых типов данных (которые не содержат ссылок на другие объекты, и соответственно, не отслеживаются сборщиком мусора), значение sys.getsizeof равно значению, возвращаемого методом __sizeof__().

id() и ctypes.string_at

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

Встроенная функция id() возвращает адрес памяти, где храниться начала объекта (сам объект является C структурой)

Чтобы считать данные по адресу памяти нужно воспользоваться функцией string_at из модуля ctypes. Ее официальное описание не очень подробное:

Теперь попробуем считать данные по адресу, который вернул нам id():

Вид шестнадцатеричного кода не очень впечатляет, но мы близки к истине.

Модель Struct

Для того чтобы представить вывод в значения, удобные для восприятия, воспользуемся еще одним модулем. Здесь нам поможет функция unpack() из модуля struct.

struct
Этот модуль производит преобразование между значениями Python и структурами на C, представленными в виде строк.

struct.unpack(формат, строка)
Разбирает строку в соответствие с данным форматов. Всегда возвращает кортеж, даже если строка содержит только один элемент. Строка должна содержать в точности то количество информации, как описано форматом.

Форматы данных, которые нам потребуются.

символ Значение C Значение Python Длина на 32битной машине
c char Строка из одного символа 1
i int int 4
l long int 4
L unsigned long int 4
d double float 8

Теперь собираем все вместе и посмотрим на внутреннее устройство некоторых типов данных.

О формате значений несложно догадаться.

Первое число (373) — количество указателей, на объект.

Как видно, число увеличилось на единицу, после того как мы создали еще одну ссылку на объект.

Второе число (136770080) — указатель (id) на тип объекта:

Третье число (1) — непосредственно содержимое объекта.

Наши догадки можно подтвердить, заглянув в исходный код CPython

Здесь PyObject_HEAD — макрос, общий для всех встроенных объектов, а ob_ival — значение типа long. Макрос PyObject_HEAD добавляет счетчик количества указателей на объект и указатель на родительский тип объекта — как раз то, что мы и видели.

Float

Число с плавающей запятой очень похоже на int, но представлено в памяти C значением типа double.

В этом легко убедиться:

Строка (Str)

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

Макрос PyObject_VAR_HEAD включает в себя PyObject_HEAD и добавляет значение long ob_ival, в котором хранится длина строки.

Четвертое значение соответствует хэшу от строки, в чем нетрудно убедиться.

Как видно, значение sstate равно 0, так что строка сейчас не кэшируется. Попробуем ее добавить в кэш:

Кортеж (Tuple)

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

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

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

Остальные базовые типы данных (unicode, list, dict, set, frozenset) можно исследовать аналогичным образом.

Что в итоге?
Тип Имя в CPython формат Формат, для вложенных объектов Длина на 32bit Длина на 64bit Память для GC*
Int PyIntObject LLl 12 24
float PyFloatObject LLd 16 24
str PyStringObject LLLli+c*(длина+1) 21+длина 37+длина
unicode PyUnicodeObject LLLLlL L*(длина+1) 28+4*длина 52+4*длина
tuple PyTupleObject LLL+L*длина 12+4*длина 24+8*длина Есть
list PyListObject L*5 L*длину 20+4*длина 40+8*длина Есть
Set/
frozenset
PySetObject L*7+(lL)*8+lL LL* длина (<=5 элементов) 100
(>5 элементов) 100+8*длина
(<=5 элементов) 200
(>5 элементов) 200+16*длина
Есть
dict PyDictObject L*7+(lLL)*8 lLL*длина (<=5 элементов) 124
(>5 элементов) 124+12*длина
(<=5 элементов) 248
(>5 элементов) 248+24*длина
Есть

* Добавляет 12 байт на 32 битной машине и 32 байта на 64 битной машине

Мы видим, что простые типы данных в Python в два-три раза больше своих прототипов на C. Разница обусловлена необходимостью хранить количество ссылок на объект и указатель на его тип (содержимое макроса PyObject_HEAD). Частично это компенсируется внутренним кэшированием, который позволяет повторно использовать ранее созданные объекты (это возможно только для неизменяемых типов).

Для строк и кортежей разница не такая значительная — добавляется некоторая постоянная величина.

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

Итак, отвечаем на вопрос в начале статьи: чтобы сохранить 1 миллион целых чисел нам потребуется 11.4 мегабайт (12*10^6 байт) на сами числа и дополнительно 3.8 мегабайт (12 + 4 + 4*10^6 байт) на кортеж, которых будет хранить на них ссылки.

UPD: Опечатки.
UPD: В подзаголовке «1 миллион целых чисел», вместо «1 миллион простых чисел»

Поймите, сколько памяти используют ваши объекты Python

Python – это фантастический язык программирования. Он также известен как довольно медленный, в основном из-за его огромной гибкости и динамических характеристик. Для многих приложений и областей это не проблема из-за их требований и различных методов оптимизации. Менее известно, что графы объектов Python (вложенные словари списков, кортежей и примитивных типов) занимают значительный объем памяти. Это может быть гораздо более серьезным ограничивающим фактором из-за его влияния на кеширование, виртуальную память, многопользовательскую работу с другими программами и в целом более быстрое исчерпание доступной памяти, которая является дефицитным и дорогим ресурсом.

Оказывается, нетривиально выяснить, сколько памяти фактически потребляется. В этой статье я расскажу о тонкостях управления памятью объекта Python и покажу, как точно измерить потребляемую память.

В этой статье я остановлюсь исключительно на CPython – основной реализации языка программирования Python. Эксперименты и выводы здесь не относятся к другим реализациям Python, таким как IronPython, Jython и PyPy.

Также я запустил числа на 64-битном Python 2.7. В Python 3 числа иногда немного отличаются (особенно для строк, которые всегда являются Unicode), но концепции одинаковы.

Практическое исследование использования памяти Python

Сначала давайте немного разберемся и получим конкретное представление о фактическом использовании памяти объектами Python.

Встроенная функция sys.getsizeof ()

Модуль sys стандартной библиотеки предоставляет функцию getsizeof () . Эта функция принимает объект (и необязательный параметр по умолчанию), вызывает метод sizeof () объекта и возвращает результат, поэтому вы также можете сделать ваши объекты инспектируемыми.

Измерение памяти объектов Python

Давайте начнем с некоторых числовых типов:

“ `python import sys

sys.getsizeof (5) 24 “ `

Интересный. Целое число занимает 24 байта.

python sys.getsizeof(5.3) 24

Хм … float также занимает 24 байта.

python from decimal import Decimal sys.getsizeof(Decimal(5.3)) 80

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

Давайте перейдем к строкам и коллекциям:

“ `python sys.getsizeof (”) 37 sys.getsizeof (‘1’) 38 sys.getsizeof (‘1234’) 41

sys.getsizeof (u ”) 50 sys.getsizeof (u’1 ‘) 52 sys.getsizeof (u’1234’) 58 “ `

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

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

Кстати, в Python 3 строки всегда имеют Unicode, а служебные данные составляют 49 байт (они где-то сохранили байт). Объект байтов имеет служебную информацию только 33 байта. Если у вас есть программа, которая обрабатывает много коротких строк в памяти, и вы заботитесь о производительности, рассмотрите Python 3.

python sys.getsizeof([]) 72 sys.getsizeof([1]) 88 sys.getsizeof([1, 2, 3, 4]) 104 sys.getsizeof([‘a long longlong string’])

В чем дело? Пустой список занимает 72 байта, но каждое дополнительное int добавляет всего 8 байтов, где размер int составляет 24 байта. Список, который содержит длинную строку, занимает всего 80 байтов.

Ответ прост. Список не содержит сами объекты int. Он просто содержит 8-байтовый (в 64-битных версиях CPython) указатель на фактический объект int. Это означает, что функция getsizeof () не возвращает фактическую память списка и всех объектов, которые он содержит, а только память списка и указатели на его объекты. В следующем разделе я представлю функцию deep_getsizeof (), которая решает эту проблему.

python sys.getsizeof(()) 56 sys.getsizeof((1,)) 64 sys.getsizeof((1, 2, 3, 4)) 88 sys.getsizeof((‘a long longlong string’,)) 64

История похожа на кортежи. Накладные расходы пустого кортежа составляют 56 байтов против 72 списка. Опять же, эта разница в 16 байтов на последовательность – это низко висящий плод, если у вас есть структура данных с большим количеством небольших неизменяемых последовательностей.

“ `python sys.getsizeof (set ()) 232 sys.getsizeof (set ([1)) 232 sys.getsizeof (set ([1, 2, 3, 4])) 232

sys.getsizeof (<>) 280 sys.getsizeof (dict (a = 1)) 280 sys.getsizeof (dict (a = 1, b = 2, c = 3)) 280 “ `

Наборы и словари якобы вообще не растут, когда вы добавляете предметы, но обратите внимание на огромные накладные расходы.

Суть в том, что у объектов Python огромные фиксированные накладные расходы. Если ваша структура данных состоит из большого количества объектов коллекций, таких как строки, списки и словари, которые содержат небольшое количество элементов каждый, вы платите много.

Функция deep_getsizeof ()

Теперь, когда я напугал вас до полусмерти и продемонстрировал, что sys.getsizeof () может только сказать вам, сколько памяти занимает примитивный объект, давайте посмотрим на более адекватное решение. Функция deep_getsizeof () рекурсивно выполняет детализацию и вычисляет фактическое использование памяти графом объектов Python.

“ `python из импорта коллекций Mapping, контейнер из sys import getsizeof

def deep_getsizeof (o, ids): «” »Найти объем памяти объекта Python

У этой функции есть несколько интересных аспектов. Он учитывает объекты, на которые ссылаются несколько раз, и учитывает их только один раз, отслеживая идентификаторы объектов. Другая интересная особенность реализации заключается в том, что она в полной мере использует абстрактные базовые классы модуля коллекций. Это позволяет функции очень лаконично обрабатывать любую коллекцию, которая реализует базовые классы Mapping или Container, вместо непосредственного обращения к множеству типов коллекций, таких как: строка, Unicode, байты, список, кортеж, dict, frozendict, OrderedDict, set, frozenset и т. Д. ,

Давайте посмотрим на это в действии:

python x = ‘1234567’ deep_getsizeof(x, set()) 44

Строка длиной 7 занимает 44 байта (37 служебных данных + 7 байтов для каждого символа).

python deep_getsizeof([], set()) 72

Пустой список занимает 72 байта (только накладные расходы).

python deep_getsizeof([x], set()) 124

Список, содержащий строку x, занимает 124 байта (72 + 8 + 44).

python deep_getsizeof([x, x, x, x, x], set()) 156

Список, содержащий строку x 5 раз, занимает 156 байтов (72 + 5 * 8 + 44).

Последний пример показывает, что deep_getsizeof () подсчитывает ссылки на один и тот же объект (строку x) только один раз, но подсчитывается указатель каждой ссылки.

Лечит или хитрости

Оказывается, что у CPython есть несколько хитростей, поэтому числа, которые вы получаете из deep_getsizeof (), не полностью отражают использование памяти программой Python.

Подсчет ссылок

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

Маленькие объекты

CPython управляет небольшими объектами (менее 256 байтов) в специальных пулах на 8-байтовых границах. Есть пулы для 1-8 байтов, 9-16 байтов и вплоть до 249-256 байтов. Когда выделяется объект размером 10, он выделяется из 16-байтового пула для объектов размером 9-16 байт. Таким образом, даже если он содержит только 10 байтов данных, он будет стоить 16 байтов памяти. Если вы выделяете 1 000 000 объектов размером 10, вы фактически используете 16 000 000 байтов, а не 10 000 000 байтов, как вы можете предположить. Эти 60% накладных расходов явно не тривиальны.

Целые

CPython хранит глобальный список всех целых чисел в диапазоне [-5, 256]. Эта стратегия оптимизации имеет смысл, потому что маленькие целые числа всплывают повсюду, и, учитывая, что каждое целое число занимает 24 байта, оно экономит много памяти для типичной программы.

Это также означает, что CPython предварительно выделяет 266 * 24 = 6384 байта для всех этих целых чисел, даже если вы не используете большинство из них. Вы можете проверить это с помощью функции id (), которая дает указатель на фактический объект. Если вы называете id (x) несколько для любого x в диапазоне [-5, 256], вы будете каждый раз получать один и тот же результат (для одного и того же целого числа). Но если вы попробуете это для целых чисел вне этого диапазона, каждый из них будет отличаться (новый объект создается на лету каждый раз).

Вот несколько примеров из этого диапазона:

“ `python id (-3) 140251817361752

id (-3) 140251817361752

id (-3) 140251817361752

id (201) 140251817366736

id (201) 140251817366736

id (201) 140251817366736 “ `

Вот несколько примеров за пределами диапазона:

“ `python id (301) 140251846945800

id (301) 140251846945776

id (-6) 140251846946960

id (-6) 140251846946936 “ `

Память Python против системной памяти

CPython является своего рода притяжательным. Во многих случаях, когда на объекты памяти в вашей программе больше нет ссылок, они не возвращаются в систему (например, небольшие объекты). Это хорошо для вашей программы, если вы выделяете и освобождаете много объектов (которые принадлежат одному и тому же 8-байтовому пулу), потому что Python не должен беспокоить систему, что относительно дорого. Но это не так хорошо, если ваша программа обычно использует X байтов и при некоторых временных условиях она использует в 100 раз больше (например, анализирует и обрабатывает большой файл конфигурации только при запуске).

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

Профилировщик памяти

Чтобы измерить и измерить фактическое использование памяти вашей программой, вы можете использовать модуль memory_profiler . Я немного поиграл с этим, и я не уверен, что доверяю результатам. Используя это очень просто. Вы декорируете функцию (может быть основной (функция 0)) с помощью декоратора @profiler, и, когда программа завершает работу, профилировщик памяти выводит на стандартный вывод удобный отчет, который показывает общее количество и изменения в памяти для каждой строки. Вот пример Программа, которую я запускал под профилировщиком:

“ `python из профиля импорта memory_profiler

@profile def main (): a = [] b = [] c = [] для i в диапазоне (100000): a.append (5) для i в диапазоне (100000): b.append (300) для i в диапазон (100000): c.append (‘123456789012345678901234567890’) del del del del c

Как вы можете видеть, 22,9 МБ дополнительной памяти занимают. Причина, по которой память не увеличивается при добавлении целых чисел как внутри, так и вне диапазона [-5, 256], а также при добавлении строки, заключается в том, что во всех случаях используется один объект. Непонятно, почему первый цикл диапазона (100000) в строке 8 добавляет 4,2 МБ, а второй в строке 10 добавляет всего 0,4 МБ, а третий цикл в строке 12 добавляет 0,8 МБ. Наконец, при удалении списков a, b и c освобождается -0.6MB для a и c, но для b добавляется 0.2MB. Я не могу иметь много смысла из этих результатов.

Вывод

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

Выучить питон

Изучите Python с нашим полным руководством по питону, независимо от того, начинаете ли вы или начинающий программист, ищущий новые навыки.

Name already in use

Python-Lessons / Theory / Part_8 / Get_information_about_memory.md

  • Go to file T
  • Go to line L
  • Copy path
  • Copy permalink
  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

Память в Python __sizeof__() sys.getsizeof()

У каждого объекта в Python есть метод __sizeof__() этот метод показывает сколько памяти, затрачивает питон в байтах на содержание этого объекта, но этот метод выводит память самого объекта и только его самого, помимо этого метода есть метод модуля sys.getsizeof() который выводит и память объекта и дополнительно количество памяти которе сборщик мусора затрачивает на хранение количества ссылок на этот объект, и потому получается больше:

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

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *