Понимаем, сколько памяти используют ваши объекты 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
Давайте начнем с некоторых числовых типов:
import sys
sys.getsizeof(5)
Интересно. Целое число занимает 24 байта.
sys.getsizeof(5.3)
Хм . float также занимает 24 байта.
from decimal import Decimal
sys.getsizeof(Decimal(5.3))
Вот это да. 80 байтов! Это действительно заставляет задуматься о том, хотите ли вы представлять большое количество вещественных чисел как числа с плавающей запятой или десятичные дроби.
Давайте перейдем к строкам и коллекциям:
sys.getsizeof('')
sys.getsizeof('1')
sys.getsizeof('1234')
sys.getsizeof(u'')
sys.getsizeof(u'1')
sys.getsizeof(u'1234')
Хорошо. Пустая строка занимает 37 байтов, и каждый дополнительный символ добавляет еще один байт. Это многое говорит о компромиссе между сохранением нескольких коротких строк, когда вы будете платить 37 байтов за каждую, а не одну длинную строку, где вы платите только один раз.
Строки Unicode ведут себя аналогично, за исключением того, что служебные данные составляют 50 байтов, и каждый дополнительный символ добавляет 2 байта. Это стоит учитывать, если вы используете библиотеки, которые возвращают строки Unicode, но ваш текст может быть представлен в виде простых строк.
Кстати, в Python 3 строки всегда имеют Unicode, а служебные данные составляют 49 байт (они где-то сохранили байт). Объект байтов имеет служебную информацию только 33 байта. Если у вас есть программа, которая обрабатывает много коротких строк в памяти, и вы заботитесь о производительности, рассмотрите Python 3.
sys.getsizeof([])
sys.getsizeof([1])
sys.getsizeof([1, 2, 3, 4])
sys.getsizeof(['a long longlong string'])
В чем дело? Пустой список занимает 72 байта, но каждый дополнительный int добавляет всего 8 байтов, где размер int составляет 24 байта. Список, который содержит длинную строку, занимает всего 80 байтов.
Ответ прост. Список не содержит сами объекты int. Он просто содержит 8-байтовый (в 64-битных версиях CPython) указатель на фактический объект int. Это означает, что функция getsizeof() не возвращает фактическую память списка и всех объектов, которые он содержит, а только память списка и указатели на свои объекты. В следующем разделе я представлю функцию deep_getsizeof(), которая решает эту проблему.
sys.getsizeof(())
sys.getsizeof((1,))
sys.getsizeof((1, 2, 3, 4))
sys.getsizeof(('a long longlong string',))
История повторяется для кортежей. Накладные расходы пустого кортежа составляют 56 байтов против 72 списка. Опять же, эта разница в 16 байтов на последовательность — это низко висящий плод, если у вас есть структура данных с большим количеством небольших неизменяемых последовательностей.
sys.getsizeof(set())
sys.getsizeof(set([1))
sys.getsizeof(set([1, 2, 3, 4]))
sys.getsizeof(<>)
sys.getsizeof(dict(a=1))
sys.getsizeof(dict(a=1, b=2, c=3))
Наборы и словари якобы вообще не растут при добавлении элементов, но отмечают огромные накладные расходы.
Суть в том, что у объектов Python огромные фиксированные накладные расходы. Если ваша структура данных состоит из большого количества объектов коллекций, таких как строки, списки и словари, которые содержат небольшое количество элементов каждый, вы много платите.
Функция deep_getsizeof()
Теперь, когда я напугал вас до полусмерти и продемонстрировал, что sys.getsizeof() может только сказать вам, сколько памяти занимает примитивный объект, давайте посмотрим на более адекватное решение. Функция deep_getsizeof() рекурсивно выполняет детализацию и вычисляет фактическое использование памяти графом объектов Python.
from collections import Mapping, Container
from sys import getsizeof
def deep_getsizeof(o, ids):
"""Find the memory footprint of a Python object
This is a recursive function that drills down a Python object graph
like a dictionary holding nested dictionaries with lists of lists
and tuples and sets.
The sys.getsizeof function does a shallow size of only. It counts each
object inside a container as pointer only regardless of how big it
really is.
:param o: the object
:param ids:
:return:
d = deep_getsizeof
if id(o) in ids:
return 0
r = getsizeof(o)
ids.add(id(o))
if isinstance(o, str) or isinstance(0, unicode):
return r
if isinstance(o, Mapping):
return r + sum(d(k, ids) + d(v, ids) for k, v in o.iteritems())
if isinstance(o, Container):
return r + sum(d(x, ids) for x in o)
return r
У этой функции есть несколько интересных аспектов. Она учитывает объекты, на которые ссылаются несколько раз, и учитывает их только один раз, отслеживая идентификаторы объектов. Другая интересная особенность реализации заключается в том, что она в полной мере использует абстрактные базовые классы модуля коллекций. Это позволяет функции очень лаконично обрабатывать любую коллекцию, которая реализует базовые классы Mapping или Container, вместо непосредственного обращения к множеству типов коллекций, таких как: строка, Unicode, байты, список, кортеж, dict, frozendict, OrderedDict, set, frozenset и т.д.
Давайте посмотрим на это в действии:
x = '1234567'
deep_getsizeof(x, set())
Строка длиной 7 занимает 44 байта (37 служебных данных + 7 байтов для каждого символа).
deep_getsizeof([], set())
Пустой список занимает 72 байта (только накладные расходы).
python deep_getsizeof ([x], set ()) 124
Список, содержащий строку x, занимает 124 байта (72 + 8 + 44).
deep_getsizeof([x, x, x, x, x], set())
Список, содержащий строку 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], вы будете каждый раз получать один и тот же результат (для одного и того же целого числа). Но если вы попробуете это для целых чисел за пределами этого диапазона, каждый из них будет отличаться (новый объект создается на лету каждый раз).
Вот несколько примеров в этом диапазоне:
140251817361752
140251817361752
140251817361752
id(201)
140251817366736
id(201)
140251817366736
id(201)
140251817366736
Вот несколько примеров за пределами диапазона:
id(301)
140251846945800
id(301)
140251846945776
140251846946960
140251846946936
Память Python против системной памяти
CPython является своего рода притяжательным. Во многих случаях, когда на объекты памяти в вашей программе больше не ссылаются, они не возвращаются в систему (например, маленькие объекты). Это хорошо для вашей программы, если вы выделяете и освобождаете много объектов (которые принадлежат одному и тому же 8-байтовому пулу), потому что Python не должен беспокоить систему, что относительно дорого. Но это не так здорово, если ваша программа обычно использует X байтов и при некоторых временных условиях она использует в 100 раз больше (например, анализирует и обрабатывает большой файл конфигурации только при запуске).
Теперь эта память 100X может быть бесполезно захвачена в вашей программе, никогда больше не использоваться и лишать систему возможности выделять ее другим программам. Ирония заключается в том, что если вы используете модуль обработки для запуска нескольких экземпляров вашей программы, вы строго ограничите количество экземпляров, которые вы можете запустить на данном компьютере.
Профилировщик памяти
Чтобы измерить и измерить фактическое использование памяти вашей программой, вы можете использовать модуль memory_profiler. Я немного поиграл с этим, и я не уверен, что доверяю результатам. Он очень прост в использовании. Вы декорируете функцию (может быть главной (0 функция)) с помощью декоратора @profiler, и когда программа завершает работу, профилировщик памяти выводит на стандартный вывод удобный отчет, который показывает общее количество и изменения в памяти для каждой строки. Вот пример программы, которую я запускал под профилировщиком:
from memory_profiler import profile
Вес некоторых типов переменных
При загрузке некоторых стандартных типов генерируется TypeLoadException
Здравствуйте. У меня возникла следующая проблема — при загрузке некоторых стандартных типов вылазит.
Почему нельзя генерировать исключения некоторых типов из своего кода
MSDN говорит следующее: Я не могу понять почему. Кто нибудь знает ответ на этот вопрос?

Как сделать, чтобы шаблонная функция не использовалась для некоторых типов?
Как сделать, чтобы для определённого типа аргумента использовалась обычная функция вместо.
Смысл некоторых переменных в коде
Это код тетриса попал мне в руки от младшего товарища ма-смотри-я-без-рук, я не совсем его понял, а.

Область видимости некоторых переменных
Доброго времени, форумчане! Подскажите, пожалуйста, по следующей тематике (visual studio 2010.
Распределение памяти в Python: сколько и в каких случаях занимают типы данных
Как устроено выделение памяти под объекты в Python, как работает очистка памяти и в чём разница в памяти на примере типов list, dict и tuple.
Идея статьи возникла после просмотра одного видео, где автор разбирает различные способы создания списка из одинаковых элементов. Меня заинтересовала эта тема, и я начал углубляться в нее. В частности, почему в том или ином случае объем занимаемой памяти отличается.
В этом материале разберем, как устроено выделение памяти под объекты в Python. Потом кратко о том, как работает очистка памяти от неиспользуемых объектов. И, наконец, о разнице в занимаемой памяти на примере типов list, dict и tuple.
Выделение памяти
Напрямую из кода память не выделяется. Вся работа по выделению памяти перекладывается на менеджеров памяти. Есть один общий менеджер «верхнего» уровня, который отвечает за выделение большого блока из выделенной программе памяти — «арена». Занимает 256Кб.
Далее арена делится на «пулы» по 4Кб. Каждый пул может содержать в себе только блоки заранее определенного для этого пула размера — от 8 байт до 512 байт. Арена может содержать в себе пулы разных размеров, а вот блоки в одном пуле всегда одного размера. И вот на уровне блоков работают менеджеры памяти каждого конкретного типа данных.
Когда менеджер определенного типа запрашивает память для объекта, он заранее знает размер и может сразу обратиться к пулу с нужным размером блоков, разместив объект в первом свободном блоке. Если же свободных блоков нет или же пулов нужного размера нет, верхний менеджер выдает новый пул из наиболее заполненной арены. Если и все арены заняты, запрашивается новая арена.
Интересно, что частично вернуть выделенную под арену память нельзя, пока в ней есть хоть один непустой пул, в котором есть хоть один непустой блок. Как блоки становятся пустыми, мы обсудим в следующем разделе.
Освобождение памяти
В Python нет необходимости в ручной очистке памяти. Если объект больше не используется, все это перекладывается на сам интерпретатор и два механизма: счетчик ссылок и сборщик мусора.
Когда переменной присваивается значение, на самом деле сначала создается объект в подходящем свободном блоке (как работает выделение памяти разобрались в прошлом разделе) и потом уже в переменную кладется ссылка на этот объект.
Счетчик ссылок
Каждый созданный объект имеет специальное поле — счетчик ссылок. Он хранит в себе количество ссылающихся на него объектов. Увеличивает свое значение, например, когда используется операция присваивания, или когда объект становится частью списка. При удалении переменной или же при использовании del счетчик ссылок уменьшается на 1. Например, при завершении работы функции, где эта переменная была объявлена.
Разберем на примере небольшой кусок кода:
a = 1000 b = a print(sys.getrefcount(1000)) # 5 del b print(sys.getrefcount(1000)) # 4
В данном случае 1000 — это неизменяемый объект, один на всю программу. После инициализации двух переменных счетчик ссылок равен 5.
Почему 5? Потому что на объект 1000 ссылаются не только эти две переменные, а все переменные со значением 1000 во всех используемых модулях. Далее удаляем переменную b и счетчик ссылок меняет свое значение на 4. Как только счетчик достигает 0, объект освобождает блок.
Сборщик мусора
У счетчика ссылок есть большой недостаток — невозможность отловить циклические ссылки. Если объект или несколько объектов ссылаются друг на друга, счетчик никогда не опустится ниже 1.
Специально для обработки таких случаев был создан модуль gc. Его работа заключается в том, чтобы периодически сканировать объекты контейнерного типа и определять наличие циклических ссылок.
Говоря о сборщике мусора, важным понятием является поколение объектов — это набор объектов, за которыми следит, а в последующем сканирует сборщик. Есть три поколения, каждое из которых сканируется с разной частотой. Чаще сканируются объекты первого поколения, т.к. туда попадают новые объекты. Как правило, такие объекты имеют маленький срок жизни и являются временными. Поэтому целесообразно их проверять больше. Если объекты прошли сканирование сборщика мусора и не были удалены, то переходят в следующее поколение.
Сканирование первого поколения начинается, когда количество созданных объектов контейнерного типа превышает количество удаленных на заданный порог. Например, сканирование второго поколения начнется, когда количество сканирований первого поколения превысит заданный порог. По умолчанию, пороги срабатывания — это 700, 10 и 10, соответственно.
Посмотреть их можно через gc.get_threshold(). Изменить через gc.set_threshold().
print(gc.get_threshold()) # (700, 10, 10)
Все это актуально для list, dict, tuple и еще для классов. Не для простых типов.
Что там по типам данных
List
Начнем сравнения с list. Поскольку он изменяемый и будет интересно посмотреть, как он ведет себя при изменении количества элементов. Список представляет из себя массив не фиксированного размера, с возможностью произвольной вставки или удаления любого элемента. Элементом списка может быть любой тип данных. С точки зрения хранения списка в памяти, он состоит из двух блоков. Один блок фиксированного размера и хранит информацию о списке. Другой же хранит ссылки на элементы и может переходить из блока в блок, если количество элементов меняется.
Размер занимаемого объектом блока памяти можно посмотреть через sys.getsizeof().
empty_list = [] print(sys.getsizeof(empty_list)) # 64
Как мы видим, пустой объект списка уже занимает 64 байта.
a = [1, 2, 3] print(sys.getsizeof(a)) # 88
При обычном создании списка через перечисление элементов он всегда будет занимать: 64 байта пустой список + 8 байт на каждый элемент, т.к. список представляет из себя массив ссылок на объекты.
b = [i for i in range(3)] print(sys.getsizeof(b)) # 96
При создании через list comprehension размер будет уже 96. Что больше, чем размер пустого списка + 8 байт на каждый элемент. Работа этого механизма сводится к вызову метода append у создаваемого объекта списка. append работает следующим образом: в зависимости от уже присутствующего в списке количества элементов он заранее резервирует больше памяти при добавлении элемента. Дополнительно выделяемый объем памяти не всегда увеличивает список вдвое. Может быть выделено место всего под несколько элементов. Например, если к списку из 8-ми элементов добавляют еще один, будет зарезервировано место еще под восемь элементов. А при добавлении к списку из 16-ти элементов будет зарезервировано место всего под 9 элементов. Это позволяет избежать затрат на изменение размера списка при частых вызовах append. При этом неиспользуемая, но уже выделенная, память недоступна для чтения.
c_tuple = (1, 2, 3) c = list(c_tuple) print(sys.getsizeof(c)) # 112
При создании списка на основе кортежа получившийся список будет занимать 112 байт. В данном случае заранее резервируется место под еще 3 элемента списка, помимо уже присутствующих в кортеже.
Итого получаем 64 байта, + 8 * 3 — это элементы из кортежа, + 8 * 3 зарезервированное место под новые элементы.
Tuple
Кортеж представляет из себя массив фиксированной длины, заданной при создании объекта. Элементами кортежа также могут быть объекты любых типов. В отличие от списка, кортеж в памяти представлен одним объектом. Поскольку нет изменяемой части, которую надо перемещать между блоками. Да, и методов для изменения элементов у кортежа так же нет. Но если сам элемент принадлежит к изменяемому типу, его все же можно изменить.
empty_tuple = () print(sys.getsizeof(empty_tuple)) # 48
Пустой кортеж занимает блок из 48 байт. Помним, что кортежи неизменяемы. Если в памяти уже есть такой же кортеж, то новый объект создан не будет. Вместо этого переменной присваивается ссылка на существующий объект.
empty_tuple = () empty_tuple_2 = () print(id(empty_tuple) == id(empty_tuple_2)) # True
На примере видно, что адреса в памяти у двух кортежей совпадают. Аналогичное сравнение для списков вернуло бы False.
a = (1, 2, 3, 4) print(sys.getsizeof(a)) # 80
В случае непустого кортежа с памятью так же все просто. Объем блока будет равен 48 байтам + по 8 байт на каждый элемент.
b_list = [1, 2, 3, 4] b = tuple(b_list) print(sys.getsizeof(b)) # 80
С созданием кортежа из списка или другой коллекции тоже нет таких неожиданностей, как со списком. Т.к. не надо закладывать место под изменение количества элементов.
Dict
Словарь представляет из себя массив ключей и массив значений, где каждый ключ связан с одним значением. На ключ накладывается ограничение по уникальности в пределах словаря. Поэтому ключами могут быть объекты только неизменяемых типов. Значением же может быть объект любого типа.
Как и списки, словари хранятся в виде двух объектов. Первый, содержит информацию о самом словаре и всегда остается в одном и том же блоке. Второй, хранит пары ключ-значение и может перемещаться между блоками при изменении размера. Но при этом пустой словарь занимает гораздо больше места.
empty_dict = <> print(sys.getsizeof(empty_dict)) # 240
Как видно в примере, словарь занимает 240 байт. При создании словаря выделяется место под несколько элементов, а не только после добавления элемента, как это было со списком.
Если вызвать метод clear, очищаются не только все элементы. Изначально зарезервированная память тоже будет освобождена.
empty_dict = <> print(sys.getsizeof(empty_dict)) # 240 empty_dict.clear() print(sys.getsizeof(empty_dict)) # 72
В итоге словарь стал занимать всего 72 байта. Гораздо меньше, чем при создании.
Как и со списком, на каждую новую пару ключ-значение весь объект не перемещается в новый блок. Чтобы избежать частых затрат на перемещение, новый блок берется с запасом на несколько элементов.
Заключение
Знать, какой объем памяти будет занимать тот или иной объект, полезно. Например, в условиях проекта или задачи указаны жесткие ограничения по памяти. Или кусок кода вызывается очень часто, необходимо понять, не будет ли выделение/освобождение памяти являться узким местом вашей системы.
Поэтому рекомендую, прикидывать примерные затраты памяти, еще на момент написания кода. И никто не отменяет переиспользование уже существующих объектов.
Если вы заинтересовались этой темой и решили углубиться в нее, то обратите внимание на следующие материалы:
Интересные проекты: Vim-плагин против глубокой вложенности кода
— Memory management in Python. Тут подробно описан механизм работы менеджеров памяти и организации блоков.
— Memory Management. Не забываем про официальную документацию.
— Garbage Collection for Python. Здесь детальнее рассказывают про алгоритм работы сборщика мусора.
— The Garbage Collector. И еще один материал о сборщике мусора.

Следите за новыми постами по любимым темам
Подпишитесь на интересующие вас теги, чтобы следить за новыми постами и быть в курсе событий.
Как узнать сколько места в памяти занимает матрица или массив?
Eсть ли какая-нибудь функция, которая позволяет узнать, сколько места в памяти занимает матрица или массив numpy?
Отслеживать
51.6k 199 199 золотых знаков 59 59 серебряных знаков 242 242 бронзовых знака
задан 20 окт 2016 в 19:02
Толкачёв Иван Толкачёв Иван
609 1 1 золотой знак 11 11 серебряных знаков 25 25 бронзовых знаков
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Можно воспользоваться методом nbytes:
In [5]: a = np.random.rand(10**6) In [6]: a.nbytes Out[6]: 8000000 In [7]: a.dtype Out[7]: dtype('float64')
это работает и для многомерных матриц:
In [11]: a = np.random.randint(0, 10, (10**3, 10**3, 10**3), dtype=np.int8) In [12]: a.shape Out[12]: (1000, 1000, 1000) In [13]: a.nbytes Out[13]: 1000000000 In [14]: a.dtype Out[14]: dtype('int8')
также можно воспользоваться стандартной функцией: sys.getsizeof()
In [33]: a = np.random.rand(10**6) In [34]: sys.getsizeof(a) Out[34]: 8000096
которая вызовет __sizeof__ для вызываемого объекта:
In [35]: a.__sizeof__() Out[35]: 8000096
getsizeof() calls the object’s __ sizeof __ method and adds an additional garbage collector overhead if the object is managed by the garbage collector.
Отслеживать
ответ дан 20 окт 2016 в 19:07
MaxU — stand with Ukraine MaxU — stand with Ukraine
149k 12 12 золотых знаков 59 59 серебряных знаков 132 132 бронзовых знака
В простом случае, numpy.array можно рассматривать как кусок памяти с соответствующими метаданными (тип, размеры, где/что лежит). .nbytes возвращает размер куска памяти (место занимаемое элементами массива—в идеализированной модели: возможно игнорируя выравнивание, вспомогательные структуры), не учитывая метаданные. sys.getsizeof() обещает вернуть размер Питон-объекта ( numpy.array() ) в байтах, что может включать в себя место и под метаданные (иногда мусор возвращается). Эти цифры следует рассматривать как оценки необходимой памяти, а фактическую память можно узнать измерениями.
21 окт 2016 в 6:31
- python
- numpy