0,0
рейтинг
16 сентября 2013 в 14:18

Разработка → Использование памяти в Python из песочницы

image

Сколько памяти занимает 1 миллион целых чисел?


Меня часто донимали размышление о том, насколько эффективно 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 << 14  # константа. в двоичным виде равна 0b100000000000000
def sys_getsizeof(obj, default = None)        
    if obj.hasattr('__sizeof__'):
        size = obj.__sizeof__()
    elif default is not None:
        return default
    else:
        raise TypeError('Объект не имеет атрибута __sizeof__')
    # Если у типа объекта установлен флаг HAVE_GC
    if type(obj).__flags__ & Py_TPFLAGS_HAVE_GC:
        size = size + размер PyGC_Head
    return size


Где 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 << 14
>>> 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) 
<type 'int'>
>>> 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
(>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 миллион простых чисел»
Сергей Романов @sleepingonee
карма
22,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (30)

  • +29
    Эцсамое. Int — не «простое число». Int — знаковое целое.
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Спасибо, я, конечно, думал про целые числа (как противоположность дробным), а про «простые».
  • 0
    Немного на ту же тему.
    Как-то надо было по быстрому обработать ~11млн простых структур (полями были целочисленные и короткие строковые поля). Хотел на python, но ему не хватило 8Г оперативки :( В итоге было переписано на C.
    Вот тогда я почувствовал, насколько python прожорлив.
    • +5
      Numpy Вам в помощь.
      • 0
        Да задача решена еще тогда была (на C памяти заняло около 2Г). Не ожидал на тот момент, что python окажется настолько прожорливым.
        • +2
          Ну может всетакие надо было заюзать итераторы? А то 11 милион структур в памяти держать — имхо не лучшее решение. Напоминает решения из серии — я не могу собрать кубик Рубика, я его молоточком а потом суперклеем :)
          • +1
            Давайте не будем обсуждать дальше решение в топике не про него) Можно в личке, если хочется.
            • +10
              Просто странно видеть заявление, что Питон прожорлив, если та же задача на Си заняла 2 Гб памяти. Если бы задача появилась на 3 года раньше и у вас была машина с 1Гб памяти, прожорлив был бы Си?

              В данном случае прожорлив не Си или Питон, а метод, которым вы решали задачу.
    • +3
      Простите, но… вы хотели запихнуть 11млн простых структур в память, чтобы их обработать?
      • 0
        Да, был файл, содержащий данные с неким подобием RTL сжатия. ~11млн это уже после обработки одинаковых значений (что-то вида 500 одинаковых значений подряд в столбце или 3000 одинаковых подряд в строке).
        • +2
          А точно нужно было в памяти всё сразу держать? Часто можно код организовывать так, чтоб это не требовалось. В Python для этого всякие плюшки есть — генераторы, модуль itertools. Значительно удобнее, чем на C :) Занимало бы тогда мегабайт 50 вместо 2 гигабайт.
          • +1
            Потому и был взят python, чтобы быстро и просто сбацать обработку :) Данные хранились поколоночно, но при этом могла быть отметка «эту ячейку повторить в строке N раз». А результат обработки нужно было сохранять построчно в другом формате. И 2Г — это на C, питон выедал 7.5Г и убивался OOM killer'ом.
            Не держать все одновременно может бы и вышло, но логика получилась бы совсем не тривиальная. А ведь кроме считывания там основной объем занимала обработка :)
            В общем, на C это легло хорошо и быстро.
  • +12
    С целыми числами не все так просто, кстати :) В CPython есть так называемые «free lists» — куски памяти, выделяемые под объекты заранее, чтоб не вызывать malloc по многу раз. Есть отдельные free lists для различных встроенных типов (словарей, кортежей, чисел и т.д.)

    Так вот, если для словарей и кортежей размер free list ограничен (1000 что-ли объектов), то в Python 2.x размер free list для чисел не ограничен. Это означает, что если в памяти в какой-то момент одновременно находится миллион разных целых чисел (экземпляров int), то они останутся там навсегда (до завершения процесса). Сборщик мусора этим всем не занимается, т.к. к сборке мусора это все отношения не имеет. Пробежались по xrange(1000000) — все ок, пробежались по range(1000000) — миллион объектов int остался в памяти навсегда.

    В одной из 3.х версий это как-то обошли.

    Ну и, понятное дело, если важно, сколько миллион чисел занимает, то его лучше хранить в array.array (или numpy.ndarray), чтоб не создавать ненужные объекты int и убрать оверхед на все эти указатели.
    • 0
      Звучит как баг и не очень понятно, при чём тут free list. Пример же на сравнении xrange() и range() сравнивает функцию-генератор и функцию, генерирующую список.
      Тут или Вы чего-то не поняли, или это реально баг в Python 2.x.

      Можно пример кода, который подтверждал бы это? Висение миллиона интов легко видно по потреблению процессом памяти.
      • +1
        Легко :)

        # примеры для Python 2.7
        def create_temp_strings():
            temp_value = [str(x) for x in xrange(10000000)]  
            
        def create_temp_ints():
            temp_value = [x*2 for x in xrange(10000000)]
        

        Если вызвать функцию create_temp_strings(), то она в пике займет мегабайт 700 памяти, но после завершения потребление памяти вернется к прежнему уровню. Если вызвать create_temp_ints(), то под Python 2.7 она займет в пике мегабайт 250, и они так и останутся занятыми после ее завершения. И там, и там используется xrange; разница в том, что в create_temp_strings() инты не находятся в памяти одновременно, а перебираются по одному. То, что все строки находятся в памяти одновременно, к проблеме не ведет, т.к. для строк объем преаллокации ограничен.

        См., например, примечания к docs.python.org/2/library/gc.html#gc.collect и deeplearning.net/software/theano/tutorial/python-memory-management.html#internal-memory-management

        Я, может, неправильно это все «free lists» называю — там для чисел вроде списки пулов, а не просто списки. Но это уже детали.
      • 0
        bugs.python.org/issue1338264 — вот issue 2005 года про это, для Python 2.4, закрыт как wont fix :) Цитата:

        Space for integer objects in particular lives in an immortal free list of unbounded size… If you don't want that, don't do that ;-)
        • 0
          Спасибо за примеры, надо будет порыть на досуге… :)
          Есть у меня на примете питоновые штуки, которые едят память как не в себя, пробую их покусать с разных сторон. Могу попробовать оформить «покусанное» в виде мыслей/вопросов о том, как исследовать потребления памяти, кстати, если интересно. Правда, выводов и готовых рецептов пока нет. :(
        • 0
          Попробовал, что-то в этом действительно есть.
          Правда, для 2.6+ похоже, что часть статей устарела (в некотором смысле), и эти free list'ы можно чистить.

          Их чистит gc.collect() при вызове с максимальной генерацией (обычно это gc.collect(2)), проверял на Вашем примере с функциями.
          Правда, как часто вызывается такая сборка мусора (и вызывается ли вообще) — я не знаю.
          • 0
            Смотрим официальную документацию (GC):

            gc.collect([generation])

            Изменения в версии 2.6. Free list-ы для некоторых встроенных типов очищаются, когда выполняется полная очистка или очистка с максимальной генерацией (2). Ввиду особенности реализации, некоторых объекты во free list-ах могут не удалятся, в частности, int и float.

            Насколько я понимаю, в нашем случае это верно. Никакого видимого эффекта после вызова gc.collect(2) не произошло.
            • 0
              Странно, я читал исходники 2.7.5, и там список int'ов вполне себе чистится (в том случае, если эти int'ы не используются, т.е. их refcount = 0).
              И вызов gc.collect(2) у меня приводил к тому, что память освобождалась (для вышеприведённого примера с функцией), проверял на Win ActiveState Python 2.7.2 x86_64.
              Может, у Вас версия другая какая?..
            • 0
              Посмотрел сейчас внимательно в код (Objects/intobject.c, функция PyInt_ClearFreeList), часть int'ов может быть не очищена при очистке free list'a, поскольку последний хранится в виде связного списка небольших массивов, и каждый массив удаляется только в том случае, если в нём остались только int'ы с нулевым количеством ссылок (т.е. один «живой» int будет держать весь массив).
              • +4
                Похоже, я был зомбирован статьями и комментариями в интернете, которые уже успели устареть. Почитал исходный код и кое-чего уяснил. Я попробую проиллюстрировать свои открытия на примерах, но сначала немного теории…

                Объекты Int (как и некоторые другие встроенные типы) поддерживают собственный аллокатор памяти. При необходимости память запрашивается вызовом malloc (в коде используется макрос PyMem_MALLOC, но это просто обертка вокруг malloc; не путать с PyMem_Malloc) блоками по ~1kb.

                Есть два связанных списка: block_list — список всех выделенных блоков памяти и free_list — список всех свободных ячеек в этих блоках. Когда создается новый Int, то первым делом (еще есть кэширование значений от -5 до 256, но не будем об этом...) проверяется указатель free_list. Если он не равен null, то Int записывается в свободную ячейку, а free_list укорачивается на один элемент. Если null, то значит, свободных ячеек больше нет. В этом случае вызывается функция fill_free_list, которая запрашивает у malloc новый блок и добавляет его ячейки в список free_list.

                Когда количество ссылок на Int становится равно нулю, его адрес добавляется в конец списка free_list.
                Посмотреть картинки и подробнее почитать обо всем этом можно здесь: www.laurentluce.com/posts/python-integer-objects-implementation/

                Также есть функция PyInt_ClearFreeList, которая отыскивает среди block_list-ов такие, которые полностью состоят из свободных ячеек, вырезает их из списка и вызывает на них malloc_free (опять через обертку PyMem_FREE). Эта функция (в первоначальном название PyInt_CompactFreeList) была добавлена в версии python 2.6 alpha 1 (см. bugs.python.org/issue1953). Следующие комментарий в начале модуля intobject.c явным образом отрицает ее существование, потому что он был написан на 6 лет раньше и с тех пор не обновлялся. Не верьте ему :).

                hg blame intobject.c
                     guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: /* Integers are quite normal objects, to make object handling uniform.
                     guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000:    (Using odd pointers to represent integers would save much space
                     guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000:    but require extra checks for this special case throughout the code.)
                       tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:    Since a typical Python program spends much of its time allocating
                     guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000:    and deallocating integers, these operations should be very fast.
                     guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000:    Therefore we use a dedicated allocation scheme with a much lower
                     guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000:    overhead (in space and time) than straight malloc(): a simple
                     guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000:    dedicated free list, filled when necessary with memory from malloc().
                       tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:
                       tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:    block_list is a singly-linked list of all PyIntBlocks ever allocated,
                       tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:    linked via their next members.  PyIntBlocks are never returned to the
                       tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:    system before shutdown (PyInt_Fini).
                       tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:
                       tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:    free_list is a singly-linked list of available PyIntObjects, linked
                       tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:    via abuse of their ob_type members.
                     guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: */
                



                Сама функция PyInt_ClearFreeList регулярно дергается сборщиком мусора. Вот так выглядит ее вызов в версии 2.7.5:

                Modules/gcmodule.c
                /* This is the main function.  Read this to understand how the
                 * collection process works. */
                static Py_ssize_t
                collect(int generation)
                {
                    …
                    здесь много кода
                    ...
                    /* Clear free list only during the collection of the highest
                     * generation */
                    if (generation == NUM_GENERATIONS-1) {
                        clear_freelists();
                    }
                
                /* Clear all free lists
                 * All free lists are cleared during the collection of the highest generation.
                 * Allocated items in the free list may keep a pymalloc arena occupied.
                 * Clearing the free lists may give back memory to the OS earlier.
                 */
                static void
                clear_freelists(void)
                {
                    (void)PyMethod_ClearFreeList();
                    (void)PyFrame_ClearFreeList();
                    (void)PyCFunction_ClearFreeList();
                    (void)PyTuple_ClearFreeList();
                #ifdef Py_USING_UNICODE
                    (void)PyUnicode_ClearFreeList();
                #endif
                    (void)PyInt_ClearFreeList();
                    (void)PyFloat_ClearFreeList();
                }
                



                Итак, обещанные примеры.
                Для наблюдения за расходом памяти я использовал memory_profiler в связке с psutil. Чтобы освободить память я напрямую дергаю PyInt_ClearFreeList (через ctypes.pythonapi) и, на всякий случай, gc.collect(2), чтобы доказать, что ничего нового не произойдет.

                Пример 1. Здесь все хорошо.
                Filename: tests/test_good.py
                
                Line #    Mem usage    Increment   Line Contents
                ================================================
                     6                             @profile
                     7     5.785 MB     0.000 MB   def func():
                     8    21.117 MB    15.332 MB       a = range(10**6)
                     9    17.301 MB    -3.816 MB       del a
                    10     5.895 MB   -11.406 MB       ctypes.pythonapi.PyInt_ClearFreeList()
                    11     5.895 MB     0.000 MB       gc.collect(2)
                



                Мы забрали память у ОС, а потом его вернули. Вот бы так было всегда…

                Пример 2. Что-то настораживает...
                Filename: tests/test_bad.py
                
                Line #    Mem usage    Increment   Line Contents
                ================================================
                     6                             @profile
                     7     5.781 MB     0.000 MB   def func():
                     8    21.117 MB    15.336 MB       a = range(10**6)
                     9    21.117 MB     0.000 MB       b = int('300')
                    10    17.301 MB    -3.816 MB       del a
                    11    17.309 MB     0.008 MB       ctypes.pythonapi.PyInt_ClearFreeList()
                    12    17.309 MB     0.000 MB       del b
                    13     5.895 MB   -11.414 MB       ctypes.pythonapi.PyInt_ClearFreeList()
                    14     5.895 MB     0.000 MB       gc.collect(2)
                



                Мы создали кучу Int-ов, затем создали еще один, а потом нашу кучу удалили. В итоге память в ОС не вернулось. Только после того как мы удалили последний Int (и, соответственно, освободился block_list, который он «держал») память наконец-то вернулась на место. Здесь вместо int('300') могли бы быть любые расчеты и прочие операции, которые создают Int-ов.

                Можно ли как-то избежать освобождения последнего block_list-а?

                Пример 2а. Грязный хак.
                Filename: tests/test_bad_hack.py
                
                Line #    Mem usage    Increment   Line Contents
                ================================================
                     6                             @profile
                     7     5.789 MB     0.000 MB   def func():
                     8    21.117 MB    15.328 MB       a = range(10**6)
                     9    21.117 MB     0.000 MB       b = int('300')
                    10    17.301 MB    -3.816 MB       del a
                    11    17.309 MB     0.008 MB       ctypes.pythonapi.PyInt_ClearFreeList()
                    12     5.785 MB   -11.523 MB       libc.malloc_trim(0)
                    13                                 #del b
                    14     5.785 MB     0.000 MB       ctypes.pythonapi.PyInt_ClearFreeList()
                    15     5.785 MB     0.000 MB       gc.collect(2)
                


                Здесь мы вызываем функцию malloc_trim из libc и все становиться на место. В примере это сработало, но я бы не стал использовать такой трюк в реальном проекте.
                Еще пару примеров можно почитать здесь:
                nuald.blogspot.ru/2013/06/memory-reclaiming-in-python.html
                bugs.python.org/msg134008

                Пример 3. Грустный..
                Filename: tests/test_ugly.py
                
                Line #    Mem usage    Increment   Line Contents
                ================================================
                     6                             @profile
                     7     5.781 MB     0.000 MB   def func():
                     8     5.781 MB     0.000 MB       i = 0
                     9     5.781 MB     0.000 MB       a = []
                    10    13.094 MB     7.312 MB       while i < 10**5:  # 10**5 чтобы было быстрее
                    11    13.094 MB     0.000 MB           a.append(i)
                    12    13.094 MB     0.000 MB           i += 1
                    13    12.711 MB    -0.383 MB       del a
                    14    12.711 MB     0.000 MB       del i
                    15    12.719 MB     0.008 MB       ctypes.pythonapi.PyInt_ClearFreeList()    
                    16    12.711 MB    -0.008 MB       libc.malloc_trim(0)
                    17    12.711 MB     0.000 MB       ctypes.pythonapi.PyInt_ClearFreeList()
                    18    12.711 MB     0.000 MB       gc.collect(2)
                



                Здесь нам не помогло ни тщательное удаление всех Int-ов, ни ClearFreeList, ни malloc_trim, ни gc.collect(2). Память осталась в распоряжение malloc-а и в ОС не вернулась.

                PS. Примеры запускал под Python 2.7.3 Linux 2.6.32-33 i686. Если Вы можете проверить их под Windows, буду очень признателен.
                • 0
                  В примере 2a в начале модуля была строка:

                  libc = ctypes.CDLL('libc.so.6')
                  

                  • 0
                    Замените на msvcrt, делов-то.
                • +1
                  Хм, не очень понятно, почему test_bad.py и test_ugly.py так себя ведут, вроде бы в реализации intobject'a есть освобождение промежуточных блоков, если их никто не держит.

                  Есть у меня подозрение, что это наведённый эффект от библиотеки memory_profile :) Доказать сейчас не возьмусь, но идея для проверки примерно такая — берём Ваш test_ugly.py, выкидываем декоратор, в каждой строчке втыкаем что-нибудь вроде time.sleep(0.1) (не втыкаем только в цикл).
                  Потребление памяти меряем внешним процессом.
                  • +1
                    Попробовал реализовать свою идею.

                    test_ugly.py:
                    import ctypes
                    import gc
                    import time
                    
                    PAUSE = 1.0
                    
                    def func():
                        time.sleep(PAUSE)
                        i = 0
                        time.sleep(PAUSE)
                        a = []
                        time.sleep(PAUSE)
                        while i < 10**5:
                            a.append(i)
                            i += 1
                        time.sleep(PAUSE)
                        del a
                        time.sleep(PAUSE)
                        del i
                        time.sleep(PAUSE)
                        ctypes.pythonapi.PyInt_ClearFreeList()    
                        time.sleep(PAUSE)
                        gc.collect(2)
                        time.sleep(PAUSE)
                    
                    if __name__ == '__main__':
                        func()
                    

                    Запускалка-измерялка b.py:
                    import psutil
                    import sys
                    import time
                    
                    def main():
                        proc = psutil.Popen([sys.executable, 'test_ugly.py'])
                        stats = []
                        while proc.poll() is None:
                            # target is alive
                            stats.append((time.time(), proc.get_memory_info().vms / 1024.0 / 1024))
                            time.sleep(0.1)
                        with open('result.txt', 'w') as out:
                            out.write('%s\n' % '\n'.join('%s %.2f' % stat for stat in stats))
                        
                    if __name__ == '__main__':
                        main()
                    


                    Построил график потребления виртуальной памяти (ActivePython 2.7.2 x86_64, Windows 7 x86_64):
                    image
                    По горизонтали — время, по вертикали — потребление памяти в мегабайтах.

                    Первый скачок — очевидно, инициализация самого интерпретатора. Второй — создание архива. Скачок вниз — освобождение. Освобождается, кстати, существенно больше, чем в Вашем примере, но явно не вся память.
                  • +1
                    Да, кстати, размер такого массива из 105 int'ов в случае моего питона занимает примерно 3 мегабайта (в смысле сумма размера list'a и размера всех int'ов), так что вроде как освобождается почти всё. Наверно, только сам объект a уходит в free list list'ов…

                    З.Ы. В сообщении выше вместо «второй — создание архива» читать «второй — создание массива» :)
    • 0
      Я так понимаю дело в реализации PyMalloc, а именно в том что он не хочет отдавать память, которая использовалась для хранения int/float обратно операционной системе после фактического уничтожения объектов, либо использовать ее для других целей, кроме как для хранения новых int/float.

      То есть, если мы создадим много объектов (a = range(10**6)), то мы забираем 11.4mb у ОС на хранение int-ов и примерно 4mb на хранение списка. Если после этого удалим «a» (del a), то ОС вернется обратно 4mb, который высвободится из-под списка, но 11.4mb останется в распоряжение питона. Эту память может будет использовать только под новые int-ы/float.
      • 0
        Этот комментарий не верен. Intobject.c получает память напрямую через malloc и также ее освобождает. См. новый комментарий выше.

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