Потребление памяти Python объектов и процессов

Я написал следующий код:

from hurry.size import size
from pysize import get_zise
import os
import psutil
def load_objects():
   process = psutil.Process(os.getpid())
   print "start method"
   process = psutil.Process(os.getpid())
   print "process consumes " + size(process.memory_info().rss)
   objects = make_a_call()
   print "total size of objects is " + (get_size(objects))
   print "process consumes " + size(process.memory_info().rss)
   print "exit method"

def main():
    process = psutil.Process(os.getpid())
    print "process consumes " + size(process.memory_info().rss)
    load_objects()
    print "process consumes " + size(process.memory_info().rss)

get_size() возвращает потребление памяти объектов с помощью этой код.

Я получаю следующие отпечатки:

process consumes 21M
start method
total size of objects is 20M
process consumes 29M
exit method
process consumes 29M
  1. как приходят объекты уничтоженные 20М если процесс уничтожил только 8М больше?
  2. если я выйду из метода, память не должна уменьшиться до 21, поскольку сборщик мусора очистит потребляемую память?

2 ответов


  1. скорее всего, это потому, что в вашем коде есть неточность.

вот полностью рабочий (python 2.7) пример, который имеет ту же проблему (я немного обновил исходный код для простоты)

from hurry.filesize import size
from pysize import get_size
import os
import psutil


def make_a_call():
    return range(1000000)

def load_objects():
    process = psutil.Process(os.getpid())
    print "start method"
    process = psutil.Process(os.getpid())
    print"process consumes ", size(process.memory_info().rss)
    objects = make_a_call()
    # FIXME
    print "total size of objects is ", size(get_size(objects))
    print "process consumes ", size(process.memory_info().rss)
    print "exit method"

def main():
    process = psutil.Process(os.getpid())
    print "process consumes " + size(process.memory_info().rss)
    load_objects()
    print "process consumes " + size(process.memory_info().rss)


main()

вот вывод:

process consumes 7M
start method
process consumes  7M
total size of objects is  30M
process consumes  124M
exit method
process consumes 124M

разница составляет ~100 МБ

и вот фиксированная версия кода:

from hurry.filesize import size
from pysize import get_size
import os
import psutil


def make_a_call():
    return range(1000000)

def load_objects():
    process = psutil.Process(os.getpid())
    print "start method"
    process = psutil.Process(os.getpid())
    print"process consumes ", size(process.memory_info().rss)
    objects = make_a_call()
    print "process consumes ", size(process.memory_info().rss)
    print "total size of objects is ", size(get_size(objects))
    print "exit method"

def main():
    process = psutil.Process(os.getpid())
    print "process consumes " + size(process.memory_info().rss)
    load_objects()
    print "process consumes " + size(process.memory_info().rss)


main()

и вот обновленный вывод:

process consumes 7M
start method
process consumes  7M
process consumes  38M
total size of objects is  30M
exit method
process consumes 124M

вы заметили разницу? Вы вычисляете размеры объектов перед измерением конечного размера процесса, и это приводит к дополнительному потреблению памяти. Давайте проверим, почему это может произойти - вот источники https://github.com/bosswissam/pysize/blob/master/pysize.py:

import sys
import inspect

def get_size(obj, seen=None):
    """Recursively finds size of objects in bytes"""
    size = sys.getsizeof(obj)
    if seen is None:
        seen = set()
    obj_id = id(obj)
    if obj_id in seen:
        return 0
    # Important mark as seen *before* entering recursion to gracefully handle
    # self-referential objects
    seen.add(obj_id)
    if hasattr(obj, '__dict__'):
        for cls in obj.__class__.__mro__:
            if '__dict__' in cls.__dict__:
                d = cls.__dict__['__dict__']
                if inspect.isgetsetdescriptor(d) or inspect.ismemberdescriptor(d):
                    size += get_size(obj.__dict__, seen)
                break
    if isinstance(obj, dict):
        size += sum((get_size(v, seen) for v in obj.values()))
        size += sum((get_size(k, seen) for k in obj.keys()))
    elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
        size += sum((get_size(i, seen) for i in obj))
    return size

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

  1. прежде всего, это поведение сильно зависит от того, используете ли вы CPython или что-то еще. Что касается CPython, это может произойти, потому что не всегда возможно немедленно вернуть память ОС.

вот хороший статьи на эту тему, цитирую:

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


  1. почему бы процессу нужно потреблять накладных превышает 8М?
  2. сбор мусора не обязательно происходит сразу. см. документацию:

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

деталь реализации CPython: CPython в настоящее время использует схема отсчета ссылок c (опционально) задержкой обнаружения циклически связанный мусор, который собирает большинство объектов, как только они стать недоступным, но не гарантированно собирать мусор содержит циклические ссылки. См. документацию gc модуль для информации о контроль сбора циклического мусора. Другой реализации действуют по-разному, и CPython может измениться. Не зависеть о немедленном завершении объектов, когда они становятся недостижимыми (так вы всегда должны закрывать файлы явно).