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

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

  • автор:

Оптимизация потребления памяти в Python

Когда дело доходит до оптимизации производительности, люди обычно сосредотачиваются только на скорости и использовании ЦП. Редко кого волнует потребление оперативной памяти, но только до тех пор, пока она не кончится.

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

Существует множество причин, по которым стоит попытаться ограничить использование памяти:

  1. Предотвращение сбоя приложения из-за ошибок нехватки оперативной памяти.
  2. Ресурсы — как ЦП, так и ОЗУ стоят денег, зачем тратить память на запуск неэффективных приложений, если есть способы уменьшить объем памяти?
  3. Данные имеют «массу«, и если их много, то они будут перемещаться медленно. Если данные должны храниться на диске, а не в ОЗУ или быстром кэше, то загрузка и обработка займет некоторое время, что повлияет на общую производительность.

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

Содержание:

  • Ищем узкие места;
    • Модуль memory_profiler ;
    • Модуль Pympler ;
    • Использование генераторов Python и/или модуль mmap .

    Ищем узкие места.

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

    Модуль memory_profiler .

    Первый инструмент, который предлагается использовать, это сторонний модуль memory_profiler . Модуль memory_profiler измеряет использование памяти конкретной функцией построчно.

    Чтобы начать использовать memory_profiler , установим его в виртуальное кружение с помощью менеджера пакетов pip вместе с пакетом psutil , который значительно повышает производительность профилировщика.

    # создаем виртуальное окружение, если нет $ python3 -m venv .venv --prompt VirtualEnv # активируем виртуальное окружение $ source .venv/bin/activate # обновляем `pip` (VirtualEnv):~$ python3 -m pip install -U pip # ставим модули `memory_profiler` и `psutil` (VirtualEnv):~$ python3 -m pip install memory_profiler psutil

    Теперь, в тестируемом сценарии необходимо украсить/пометить функцию, которую нужно протестировать, при помощью декоратора @profile .

    # абстрактный код @profile def memory_intensive(): . if __name__ == '__main__': memory_intensive() 

    Далее запускаем тестируемый сценарий test-code.py следующим образом:

    (VirtualEnv):~$ python3 -m memory_profiler test-code.py # Line # Mem usage Increment Occurrences Line Contents # ============================================================ # 15 39.113 MiB 39.113 MiB 1 @profile # 16 def memory_intensive(): # 17 46.539 MiB 7.426 MiB 1 small_list = [None] * 1000000 # 18 122.852 MiB 76.312 MiB 1 big_list = [None] * 10000000 # 19 46.766 MiB -76.086 MiB 1 del big_list # 20 46.766 MiB 0.000 MiB 1 return small_list 

    Вывод показывает использование/распределение памяти построчно для декорированной функции — в данном случае memory_intensive() , которая преднамеренно создает и удаляет большие списки. Первый столбец (Line) представляет номер строки кода, который был профилирован, второй столбец (Mem usage) — использование памяти интерпретатором Python после выполнения этой строки. Третий столбец (Increment) представляет разницу в памяти текущей строки по отношению к последней. Последний столбец (Line Contents) печатает профилированный код.

    Примечание. Если импортировать декоратор @profile из модуля memory_profiler , то скрипт можно запустить без указания -m memory_profiler (как обычный сценарий $ python3 test.py ).

    # test.py from memory_profiler import profile @profile def my_func(): a = [1] * (10 ** 6) b = [2] * (2 * 10 ** 7) del b return a if __name__ == '__main__': my_func() 

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

    >>> import sys >>> sys.getsizeof(1) # 28 >>> sys.getsizeof(2**30) # 32 >>> sys.getsizeof(2**60) # 36 >>> sys.getsizeof("a") # 50 >>> sys.getsizeof("aa") # 51 >>> sys.getsizeof("aaa") # 52 >>> sys.getsizeof([]) # 56 >>> sys.getsizeof([1]) # 64 # Обратите внимание на вывод. Пустой список # равен 56, а каждое значение внутри равно 28. >>> sys.getsizeof([1, 2, 3, 4, 5]) # 96 

    Из примеров видо, что с простыми целыми числами, когда пересекается порог, то к размеру добавляется 4 байта. Точно так же с простыми строками, при добавлении нового символа, добавляется один дополнительный байт. Но со списками это не работает — sys.getsizeof не «обходит» структуру данных и возвращает только размер родительского объекта, в данном случае списка.

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

    Модуль Pympler .

    Установим модуль Pympler в виртуальное окружение:

    # ставим модуль `Pympler` (VirtualEnv):~$ python3 -m pip install Pympler

    Для исследования того, сколько памяти потребляют определенные объекты Python можно использовать функцию pympler.asizeof.asizeof() . В отличие от от встроенной функции sys.getsizeof() , она рекурсивно изменяет размеры объектов. Кроме того, этот модуль также имеет функцию pympler.asizeof.asized() , которая дает дополнительную разбивку по размеру отдельных компонентов объекта.

    >>> from pympler import asizeof >>> asizeof.asizeof([1, 2, 3, 4, 5])) # 256 >>> asizeof.asized([1, 2, [3, 4], "string"], detail=1).format() # [1, 2, [3, 4], 'string'] size=344 flat=88 # [3, 4] size=136 flat=72 # 'string' size=56 flat=56 # 1 size=32 flat=32 # 2 size=32 flat=32 

    Как экономить оперативную память.

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

    Списки list в Python — один из наиболее требовательных к памяти вариантов, когда речь идет о хранении массивов значений.

    Создадим простую функцию allocate() , которая создает список чисел, используя указанный размер size . Чтобы измерить, сколько памяти он занимает, используем модуль memory_profiler . Он даст объем памяти во время выполнения этой функции с интервалом в 0,2 секунды.

    >>> from memory_profiler import memory_usage >>> def allocate(size): . some_var = [n for n in range(size)] # `1e7` равно 10 в степени 7 usage = memory_usage((allocate, (int(1e7),))) # Использование с течением времени >>> usage # [ # 43.671875, 43.73828125, # 176.8671875, 314.8828125, # 343.9140625, 43.8671875 # ] # Пиковое использование >>> max(usage) # 343.9140625 

    Можно заметить, что для создания списка из 10 миллионов чисел требуется более 350 МБ памяти. Это однозначно много для списка цифр. Можно ли сделать лучше?

    >>> from memory_profiler import memory_usage >>> import array >>> def allocate(size): . some_var = array.array('l', range(size)) # запускаем `allocate()` >>> usage = memory_usage((allocate, (int(1e7),))) # Использование с течением времени >>> usage # [ # 39.71484375, 39.71484375, 55.34765625, # 71.14453125, 86.54296875, 101.49609375, # 39.73046875 # ] # Пиковое использование >>> max(usage) # 101.49609375 

    В этом примере использовался модуль array , который может хранить примитивы, такие как целые числа или символы. Из результатов видно, что использование памяти достигло пика чуть более 100 МБ. Это огромная разница по сравнению с использованием списка. Можно дополнительно уменьшить использование памяти, указав при создании массива соответствующую точность хранимого типа.

    Одним из основных недостатков использования модуля array в качестве контейнера данных является то, что он поддерживает не так много типов. Если планируется выполнять множество математических операций с данными, то лучше использовать массивы | NumPy |:

    >>> from memory_profiler import memory_usage >>> import numpy as np >>> def allocate(size): . some_var = np.arange(size) # запускаем `allocate()` >>> usage = memory_usage((allocate, (int(1e7),))) # Использование с течением времени >>> usage # [ # 52.0625, 52.25390625, . # 97.28515625, 107.28515625, . # 123.28515625, 52.0625 # ] # Пиковое использование >>> max(usage) # 123.28515625 

    Массивы NumPy работают довольно неплохо, использование памяти с пиковым размером массива — 123 МБ, что немного больше, чем array . Но с NumPy можно воспользоваться преимуществами быстрых математических функций, а также типов, которые не поддерживаются array , например, таких как комплексные числа.

    Также можно внести некоторые улучшения в размер отдельных объектов, которые определяются пользовательскими классами. Это можно сделать с помощью атрибута класса __slots__ , который используется для явного объявления свойств класса. Объявление __slots__ в классе также имеет хороший побочный эффект, заключающийся в запрете создания атрибутов __dict__ и __weakref__ :

    >>> from pympler import asizeof >>> class Normal: . pass . >>> class Smaller: . __slots__ = () . >>> asizeof.asized(Normal(), detail=1).format() # ' size=152 flat=48 # __dict__ size=104 flat=104\n __class__ size=0 flat=0' >>> asizeof.asized(Smaller(), detail=1).format() # ' size=32 flat=32 # __class__ size=0 flat=0' 

    Здесь видно, насколько меньшим оказался экземпляр класса Smaller() . Отсутствие __dict__ удаляет целых 104 байта из каждого экземпляра, что может сэкономить огромное количество памяти при создании экземпляров миллионов значений.

    Приведенные выше приемы должны помочь при работе с числовыми значениями, а также с объектами классов. А что делать со строками str Python? Как правило, это зависит от того, что с ними будет делать программа. Если необходим поиск в огромном количестве строковых значений, то, как видели ранее, использование списка list — очень плохая идея. Использование множества set может быть немного более подходящим, если важна скорость выполнения, но он будет потреблять еще больше оперативной памяти. Лучшим вариантом может быть использование оптимизированной структуры данных, такой как trie , особенно для статических наборов данных, которые используются, например, для запросов. Для этого уже есть библиотека, а также для многих других древовидных структур данных, некоторые из которых можно найти на https://github.com/pytries.

    Использование генераторов Python и/или модуль mmap .

    Самый простой способ сэкономить оперативную память — вообще не использовать ее. Очевидно, что нельзя полностью избежать использования ОЗУ, но можно избежать загрузки всего набора данных сразу и вместо этого работать с данными постепенно, где это возможно. Самый простой способ добиться этого — использовать генераторы, которые вычисляют элементы по требованию, а не все сразу. Например, встроенная функция open() имеет поведение генератора, если не использовать методы файлового объекта.

    from memory_profiler import profile @profile def read_file(file): with open(file, "r") as file: for line in file: print(line.strip()) if __name__ == '__main__': read_file("some-data.txt") 
    (VirtualEnv) :~$ python3 test.py . . # здесь печатаются строки из файла . Filename: test.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 3 40.0 MiB 40.0 MiB 1 @profile() 4 def read_file(file): 5 40.0 MiB 0.0 MiB 1 with open(file, "r") as file: 6 40.0 MiB 0.0 MiB 1328 for line in file: 7 40.0 MiB 0.0 MiB 1327 print(line.strip()) 

    О чудо, память использует только декоратор @profile !

    Более сильным инструментом являются файлы с отображением в памяти, которые позволяют загружать только части данных из файла. Для таких целей стандартная библиотека Python предоставляет модуль mmap . Отображаемые в памяти файлы ведут себя как файлы и байтовые массивы одновременно. Например их можно использовать как с файловыми операциями, такими как чтение, поиск или запись, так и со строковыми операциями:

    import mmap with open("some-data.txt", "r") as file: with mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) as m: print(f"Чтение с использованием метод '.read': m.read(15)>") m.seek(0) print(f"Чтение с использованием среза: m[:15]>") # Чтение с использованием метод '.read': b'Lorem ipsum dol' # Чтение с использованием среза: b'Lorem ipsum dol' 

    Загрузка/чтение отображаемого в память файла очень проста. Сначала открываем файл для чтения, как обычно. Затем используем файловый дескриптор файла ( file.fileno() ) для создания из него отображаемого в память файла. Оттуда можно получить доступ к его данным как с файловыми операциями, такими как чтение, так и со строковыми операциями, такими как срез.

    В большинстве случаев используется только чтение файла, как показано выше, но также можно производить запись:

    import mmap import re with open("some-data.txt", "r+") as file: with mmap.mmap(file.fileno(), 0) as m: # Ищем слова, начинающиеся с заглавной буквы pattern = re.compile(rb'\b[A-Z].*?\b') for match in pattern.findall(m): print(match) # b'Lorem' # b'Morbi' # b'Nullam' # . # теперь удалим первые 10 символов start = 0 end = 10 length = end - start new_size = len(m) - length m.move(start, end, len(m) - end) m.flush() file.truncate(new_size) 

    Первое отличие в коде, это изменение режима доступа на «r+» , что означает как чтение, так и запись. Cначала читаем из файла и используя RegEx ищем все слова, начинающихся с заглавной буквы. После этого демонстрируем удаление данных из файла. Это не так просто, как чтение и поиск, т.к. при удалении части содержимого необходима настройка размера файла в оперативной памяти. Для этого используется метод move(dest, src, count) , который копирует размер — конечные байты данных из конца индекса в начало индекса, что в данном случае приводит к удалению первых 10 байтов.

    Как измерить количество используемой методом памяти Python?

    Нужно измерить количество потребляемой памяти.
    Пытаюсь измерить с помощью memory_profiler , но его PyCharm почему-то не видит (from memory_profiler import memory_usage написал). Теперь вопрос, как получить именно используемую память?
    Память вычисляется с помощью memory_profiler.memory_usage(), но почему-то не работает, если писать так:

    import memory_profiler def method(g): x = [] for i in range(g): x.append(i) return x y = [] for j in range(len(10)): y.append(memory_profiler.memory_usage(method(10000))) 

    Научный форум dxdy

    Доброе утро. Я написал программу которая правильно выполняет поставленную задачу, насколько я могу судить по предоставленному примеру ввода и вывода, но не удовлетворяет требованию: Ограничение памяти 64Mb

    Как бы мне выяснить сколько памяти потребляет моя программа? А ещё лучше какая часть моей программы является самой прожорливой? Нашёл одно возможное решение, но оно использует некую стороннюю библиотеку:
    https://stackoverflow.com/questions/985 . n-function
    Может как то средствами стандартной возможно обойтись?

    На всякий случай код программы, один а иногда два цикла выполняются в условиях ограничений по памяти, присваивание всем переменным нулей в конце цикла это моя попытка «очистить память»

    Используется синтаксис Python

    from pymorphy2 import MorphAnalyzer

    for i in range ( 9 ) :
    adjf , noun = input ( ) . strip ( ) . split ( )
    noun_a = MorphAnalyzer ( ) . parse ( noun ) [ 0 ]
    adjf_a = MorphAnalyzer ( ) . parse ( adjf ) [ 0 ]
    if str ( noun_a. tag ) . split ( ‘,’ ) [ 0 ] == ‘ADJF’ :
    noun , adjf = adjf , noun
    noun_a , adjf_a = adjf_a , noun_a
    a , b , c , d = tuple ( str ( noun_a. tag ) . split ( ‘,’ ) )
    c1 , c2 = c. split ( )
    if c2 == ‘plur’ :
    print ( adjf_a. inflect ( { c2 , d } ) . methods_stack [ 0 ] [ 1 ] , end = ‘ ‘ )
    else :
    print ( adjf_a. inflect ( { c1 , c2 , d } ) . methods_stack [ 0 ] [ 1 ] , end = ‘ ‘ )
    print ( noun )
    adjf = 0
    noun = 0
    noun_a = 0
    adjf_a = 0
    a = 0
    b = 0
    c = 0
    d = 0
    c1 = 0
    c2 = 0

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

    image

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

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

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

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

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

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

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

    Py_TPFLAGS_HAVE_GC = 1  

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

    typedef union _gc_head < struct < union _gc_head *gc_next; union _gc_head *gc_sourcev; Py_ssize_t gc_refs; >gc; long double dummy; > PyGC_Head; 

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

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

    >>> import sys >>> GC_FLAG = 1 >> sys.getsizeof(1) 12 >>> (1).__sizeof__() 12 >>> bool(type(1).__flags__ & GC_FLAG) False >>> sys.getsizeof(1.1) 16 >>> (1.1).__sizeof__() 16 >>> bool(type(1.1).__flags__ & GC_FLAG) False >>> sys.getsizeof('') 21 >>> ''.__sizeof__() 21 >>> bool(type('').__flags__ & GC_FLAG) False >>> sys.getsizeof('hello') 26 >>> sys.getsizeof(tuple()) 24 >>> tuple().__sizeof__() 12 >>> bool(type(tuple()).__flags__ & GC_FLAG) True >>> sys.getsizeof(tuple((1, 2, 3))) 36 

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

    id() и ctypes.string_at

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

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

    >>> obj = 1 >>> id(obj) 158020320 

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

    ctypes.string_at(адрес[, длина])
    Это функция возвращает строку, с началом в ячейки памяти «адрес». Если «длина» не указана, то считается что строка zero-terminated,

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

    >>> import ctypes >>> obj = 1 >>> sys.getsizeof(obj) 12 >>> ctypes.string_at(id(obj), 12) 'u\x01\x00\x00 \xf2&\x08\x01\x00\x00\x003\x01\x00\x00 \xf2&\x08\x00\x00\x00\x001\x00\x00\x00' 

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

    Модель 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

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

    Int
    >>> obj = 1 >>> sys.getsizeof(obj), obj.__sizeof__() (12, 12) >>> struct.unpack('LLl', ctypes.string_at(id(obj), 12)) (373, 136770080, 1) 

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

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

    >>> obj2 = obj >>> struct.unpack('LLl', ctypes.string_at(id(obj), 12)) (374, 136770080, 1) 

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

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

    >>> type(obj) >>> id(type(obj) ) 136770080 

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

    >>> obj = 1234567 >>> struct.unpack('LLl', ctypes.string_at(id(obj), 12)) (1, 136770080, 1234567) 

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

    typedef struct < PyObject_HEAD long ob_ival; >PyIntObject; 

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

    Float

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

    typedef struct < PyObject_HEAD double ob_fval; >PyFloatObject; 

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

    >>> obj = 1.1 >>> sys.getsizeof(obj), obj.__sizeof__() (16, 16) >>> struct.unpack('LLd', ctypes.string_at(id(obj), 16) (1, 136763968, 1.1) 
    Строка (Str)

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

    typedef struct < PyObject_VAR_HEAD long ob_shash; # хэш от строки int ob_sstate; # находится ли в кэше? char ob_sval[1]; # содержимое строки + нулевой байт >PyStringObject; 

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

    >>> obj = 'hello world' >>> sys.getsizeof(obj), obj.__sizeof__() (32, 32) >>> struct.unpack('LLLli' + 'c' * (len(obj) + 1), ctypes.string_at(id(obj), 4*5 + len(obj) + 1)) (1, 136790112, 11, -1500746465, 0, 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\x00') 

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

    >>> hash(obj) -1500746465 

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

    >>> intern(obj) 'hello world' >>> struct.unpack('LLLli' + 'c' * (len(obj) + 1), ctypes.string_at(id(obj), 4*5 + len(obj) + 1)) (2, 136790112, 11, -1500746465, 1, 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\x00') 
    Кортеж (Tuple)

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

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

    typedef struct < PyObject_VAR_HEAD PyObject *ob_item[1]; >PyTupleObject; 
    >>> obj = (1,2,3) >>> sys.getsizeof(obj), obj.__sizeof__() (36, 24) >>> struct.unpack('LLL'+'L'*len(obj), ctypes.string_at(id(obj), 12+4*len(obj))) (1, 136532800, 3, 146763112, 146763100, 146763088) >>> for i in obj: print i, id(i) 1 146763112 2 146763100 3 146763088 

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

    Остальные базовые типы данных (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+8*длина ( (>5 элементов) 200+16*длина Есть
    dict PyDictObject L*7+(lLL)*8 lLL*длина ( (>5 элементов) 124+12*длина ( (>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
    • Программирование

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

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