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

Gigi Sayfan

Gigi Sayfan Last updated May 20, 2022

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Функция deep_getsizeof()

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

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

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

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

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

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

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

Список, содержащий строку 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 против системной памяти

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

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

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

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

Как вы можете видеть, занято 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: сколько и в каких случаях занимают типы данных

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

В этом материале разберем, как устроено выделение памяти под объекты в Python. Потом кратко о том, как работает очистка памяти от неиспользуемых объектов. И, наконец, о разнице в занимаемой памяти на примере типов list, dict и tuple.

Выделение памяти

Напрямую из кода память не выделяется. Вся работа по выделению памяти перекладывается на менеджеров памяти. Есть один общий менеджер «верхнего» уровня, который отвечает за выделение большого блока из выделенной программе памяти — «арена». Занимает 256Кб.

Далее арена делится на «пулы» по 4Кб. Каждый пул может содержать в себе только блоки заранее определенного для этого пула размера — от 8 байт до 512 байт. Арена может содержать в себе пулы разных размеров, а вот блоки в одном пуле всегда одного размера. И вот на уровне блоков работают менеджеры памяти каждого конкретного типа данных.

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

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

Освобождение памяти

В Python нет необходимости в ручной очистке памяти. Если объект больше не используется, все это перекладывается на сам интерпретатор и два механизма: счетчик ссылок и сборщик мусора.

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

Счетчик ссылок

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

Разберем на примере небольшой кусок кода:

В данном случае 1000 — это неизменяемый объект, один на всю программу. После инициализации двух переменных счетчик ссылок равен 5.

Почему 5? Потому что на объект 1000 ссылаются не только эти две переменные, а все переменные со значением 1000 во всех используемых модулях. Далее удаляем переменную b и счетчик ссылок меняет свое значение на 4. Как только счетчик достигает 0, объект освобождает блок.

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

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

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

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

Сканирование первого поколения начинается, когда количество созданных объектов контейнерного типа превышает количество удаленных на заданный порог. Например, сканирование второго поколения начнется, когда количество сканирований первого поколения превысит заданный порог. По умолчанию, пороги срабатывания — это 700, 10 и 10, соответственно.

Посмотреть их можно через gc.get_threshold(). Изменить через gc.set_threshold().

Все это актуально для list, dict, tuple и еще для классов. Не для простых типов.

Что там по типам данных

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

Размер занимаемого объектом блока памяти можно посмотреть через sys.getsizeof().

Как мы видим, пустой объект списка уже занимает 64 байта.

При обычном создании списка через перечисление элементов он всегда будет занимать: 64 байта пустой список + 8 байт на каждый элемент, т.к. список представляет из себя массив ссылок на объекты.

При создании через list comprehension размер будет уже 96. Что больше, чем размер пустого списка + 8 байт на каждый элемент. Работа этого механизма сводится к вызову метода append у создаваемого объекта списка. append работает следующим образом: в зависимости от уже присутствующего в списке количества элементов он заранее резервирует больше памяти при добавлении элемента. Дополнительно выделяемый объем памяти не всегда увеличивает список вдвое. Может быть выделено место всего под несколько элементов. Например, если к списку из 8-ми элементов добавляют еще один, будет зарезервировано место еще под восемь элементов. А при добавлении к списку из 16-ти элементов будет зарезервировано место всего под 9 элементов. Это позволяет избежать затрат на изменение размера списка при частых вызовах append. При этом неиспользуемая, но уже выделенная, память недоступна для чтения.

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

Итого получаем 64 байта, + 8 * 3 — это элементы из кортежа, + 8 * 3 зарезервированное место под новые элементы.

Tuple

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

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

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

В случае непустого кортежа с памятью так же все просто. Объем блока будет равен 48 байтам + по 8 байт на каждый элемент.

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

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

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

Как видно в примере, словарь занимает 240 байт. При создании словаря выделяется место под несколько элементов, а не только после добавления элемента, как это было со списком.

Если вызвать метод clear, очищаются не только все элементы. Изначально зарезервированная память тоже будет освобождена.

В итоге словарь стал занимать всего 72 байта. Гораздо меньше, чем при создании.

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

Заключение

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

Поэтому рекомендую, прикидывать примерные затраты памяти, еще на момент написания кода. И никто не отменяет переиспользование уже существующих объектов.

Если вы заинтересовались этой темой и решили углубиться в нее, то обратите внимание на следующие материалы:

Интересные проекты: Vim-плагин против глубокой вложенности кода

— Memory management in Python. Тут подробно описан механизм работы менеджеров памяти и организации блоков.

— Memory Management. Не забываем про официальную документацию.

— Garbage Collection for Python. Здесь детальнее рассказывают про алгоритм работы сборщика мусора.

How do I determine the size of an object in Python?

How do I get the size occupied in memory by an object in Python?

Mateen Ulhaq's user avatar

16 Answers 16

Just use the sys.getsizeof function defined in the sys module.

sys.getsizeof(object[, default]) :

Return the size of an object in bytes. The object can be any type of object. All built-in objects will return correct results, but this does not have to hold true for third-party extensions as it is implementation specific.

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

The default argument allows to define a value which will be returned if the object type does not provide means to retrieve the size and would cause a TypeError .

getsizeof calls the object’s __sizeof__ method and adds an additional garbage collector overhead if the object is managed by the garbage collector.

See recursive sizeof recipe for an example of using getsizeof() recursively to find the size of containers and all their contents.

Usage example, in python 3.0:

If you are in python < 2.6 and don’t have sys.getsizeof you can use this extensive module instead. Never used it though.

Neuron's user avatar

How do I determine the size of an object in Python?

The answer, "Just use sys.getsizeof ", is not a complete answer.

That answer does work for builtin objects directly, but it does not account for what those objects may contain, specifically, what types, such as custom objects, tuples, lists, dicts, and sets contain. They can contain instances each other, as well as numbers, strings and other objects.

A More Complete Answer

Using 64-bit Python 3.6 from the Anaconda distribution, with sys.getsizeof , I have determined the minimum size of the following objects, and note that sets and dicts preallocate space so empty ones don’t grow again until after a set amount (which may vary by implementation of the language):

How do you interpret this? Well say you have a set with 10 items in it. If each item is 100 bytes each, how big is the whole data structure? The set is 736 itself because it has sized up one time to 736 bytes. Then you add the size of the items, so that’s 1736 bytes in total

Some caveats for function and class definitions:

Note each class definition has a proxy __dict__ (48 bytes) structure for class attrs. Each slot has a descriptor (like a property ) in the class definition.

Slotted instances start out with 48 bytes on their first element, and increase by 8 each additional. Only empty slotted objects have 16 bytes, and an instance with no data makes very little sense.

Also, each function definition has code objects, maybe docstrings, and other possible attributes, even a __dict__ .

Also note that we use sys.getsizeof() because we care about the marginal space usage, which includes the garbage collection overhead for the object, from the docs:

getsizeof() calls the object’s __sizeof__ method and adds an additional garbage collector overhead if the object is managed by the garbage collector.

Also note that resizing lists (e.g. repetitively appending to them) causes them to preallocate space, similarly to sets and dicts. From the listobj.c source code:

Historical data

Python 2.7 analysis, confirmed with guppy.hpy and sys.getsizeof :

Note that dictionaries (but not sets) got a more compact representation in Python 3.6

I think 8 bytes per additional item to reference makes a lot of sense on a 64 bit machine. Those 8 bytes point to the place in memory the contained item is at. The 4 bytes are fixed width for unicode in Python 2, if I recall correctly, but in Python 3, str becomes a unicode of width equal to the max width of the characters.

And for more on slots, see this answer.

A More Complete Function

We want a function that searches the elements in lists, tuples, sets, dicts, obj.__dict__ ‘s, and obj.__slots__ , as well as other things we may not have yet thought of.

We want to rely on gc.get_referents to do this search because it works at the C level (making it very fast). The downside is that get_referents can return redundant members, so we need to ensure we don’t double count.

Classes, modules, and functions are singletons — they exist one time in memory. We’re not so interested in their size, as there’s not much we can do about them — they’re a part of the program. So we’ll avoid counting them if they happen to be referenced.

We’re going to use a blacklist of types so we don’t include the entire program in our size count.

To contrast this with the following whitelisted function, most objects know how to traverse themselves for the purposes of garbage collection (which is approximately what we’re looking for when we want to know how expensive in memory certain objects are. This functionality is used by gc.get_referents .) However, this measure is going to be much more expansive in scope than we intended if we are not careful.

For example, functions know quite a lot about the modules they are created in.

Another point of contrast is that strings that are keys in dictionaries are usually interned so they are not duplicated. Checking for id(key) will also allow us to avoid counting duplicates, which we do in the next section. The blacklist solution skips counting keys that are strings altogether.

Whitelisted Types, Recursive visitor

To cover most of these types myself, instead of relying on the gc module, I wrote this recursive function to try to estimate the size of most Python objects, including most builtins, types in the collections module, and custom types (slotted and otherwise).

This sort of function gives much more fine-grained control over the types we’re going to count for memory usage, but has the danger of leaving important types out:

And I tested it rather casually (I should unittest it):

This implementation breaks down on class definitions and function definitions because we don’t go after all of their attributes, but since they should only exist once in memory for the process, their size really doesn’t matter too much.

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

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