Вопрос-Ответ

Releasing memory in Python

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

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


  1. Если я запущу в интерпретаторе,


    foo = ['bar' for _ in xrange(10000000)]

    реальная память, используемая на моей машине, увеличивается до 80.9mb. Затем я,


    del foo

    реальная память сокращается, но только до 30.4mb. Интерпретатор использует 4.4mb базовый уровень, так в чем преимущество отсутствия освобождения 26mb памяти для ОС? Это потому, что Python "планирует заранее", думая, что вы снова сможете использовать такой объем памяти?


  2. Почему это происходит, в частности, 50.5mb - на основе какого объема освобождается память?


  3. Есть ли способ заставить Python освободить всю память, которая была использована (если вы знаете, что больше не будете использовать столько памяти)?


ПРИМЕЧАНИЕ Этот вопрос отличается от Как я могу явно освободить память в Python? потому что этот вопрос в первую очередь касается увеличения использования памяти по сравнению с базовым уровнем даже после того, как интерпретатор освободил объекты с помощью сборки мусора (с использованием gc.collect или нет).

Переведено автоматически
Ответ 1

Я предполагаю, что вопрос, который вас действительно волнует здесь, это:


Есть ли способ заставить Python освободить всю память, которая была использована (если вы знаете, что больше не будете использовать столько памяти)?


Нет, этого не происходит. Но есть простое решение: дочерние процессы.

Если вам нужно 500 МБ временного хранилища на 5 минут, но после этого вам нужно работать еще 2 часа и вы никогда больше не будете использовать такой объем памяти, запустите дочерний процесс для выполнения работы, требующей больших затрат памяти. Когда дочерний процесс завершает работу, память освобождается.

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

Во-первых, самый простой способ создать дочерний процесс - это использовать concurrent.futures (или, для версии 3.1 и более ранних версий, futures backport в PyPI):

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
result = executor.submit(func, *args, **kwargs).result()

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

Затраты составляют:


  • Запуск процесса происходит довольно медленно на некоторых платформах, особенно в Windows. Здесь мы говорим о миллисекундах, а не о минутах, и если вы раскручиваете один дочерний элемент для выполнения работы продолжительностью 300 секунд, вы даже не заметите этого. Но это не бесплатно.

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

  • Отправка больших объемов данных между процессами может быть медленной. Опять же, если вы говорите об отправке более 2 Тыс. аргументов и получении обратно 64 тыс. результатов, вы даже не заметите этого, но если вы отправляете и получаете большие объемы данных, вам захочется использовать какой-либо другой механизм (файл, mmapped или иной; API с общей памятью в multiprocessing; и т.д.).

  • Отправка больших объемов данных между процессами означает, что данные должны быть доступны для извлечения (или, если вы помещаете их в файл или общую память, struct с возможностью или в идеале с ctypes возможностью).

Ответ 2

Объем памяти, выделенной в куче, может быть слишком большим. Это осложняется внутренней оптимизацией Python для выделения небольших объектов (PyObject_Malloc) в пулах размером 4 КБ, классифицированных по размерам выделения, кратным 8 байтам - до 256 байт (512 байт в 3.3). Сами пулы находятся на аренах объемом 256 КБ, поэтому, если используется только один блок в одном пуле, вся арена объемом 256 КБ освобождена не будет. В Python 3.3 распределитель небольших объектов был переключен на использование анонимных карт памяти вместо кучи, поэтому он должен лучше справляться с освобождением памяти.

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

Попробуйте вот так и скажите мне, что у вас получится. Вот ссылка на psutil.Process.memory_info.

import os
import gc
import psutil

proc = psutil.Process(os.getpid())
gc.collect()
mem0 = proc.memory_info().rss

# create approx. 10**7 int objects and pointers
foo = ['abc' for x in range(10**7)]
mem1 = proc.memory_info().rss

# unreference, including x == 9999999
del foo, x
mem2 = proc.memory_info().rss

# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.memory_info().rss

pd = lambda x2, x1: 100.0 * (x2 - x1) / mem0
print "Allocation: %0.2f%%" % pd(mem1, mem0)
print "Unreference: %0.2f%%" % pd(mem2, mem1)
print "Collect: %0.2f%%" % pd(mem3, mem2)
print "Overall: %0.2f%%" % pd(mem3, mem0)

Вывод:

Allocation: 3034.36%
Unreference: -752.39%
Collect: -2279.74%
Overall: 2.23%

Редактировать:

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

Среда выполнения C (например, glibc, msvcrt) сжимает кучу, когда непрерывное свободное пространство наверху достигает постоянного, динамического или настраиваемого порога. С помощью glibc вы можете настроить это с помощью mallopt (M_TRIM_THRESHOLD). Учитывая это, неудивительно, что куча уменьшится больше - даже намного больше - чем блок, который вы используете free.

В 3.x range не создается список, поэтому приведенный выше тест не создаст 10 миллионов int объектов. Даже если бы это произошло, int тип в 3.x по сути является 2.x long, который не реализует freelist .

Ответ 3

эриксун ответил на вопрос № 1, а я ответил на вопрос № 3 (исходный # 4), но теперь давайте ответим на вопрос № 2:


Почему он освобождает именно 50,5 МБ - на основе какого объема освобождается память?


В конечном счете, это основано на целой серии совпадений внутри Python и malloc которые очень трудно предсказать.

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

Или вы можете измерять используемые страницы, на которых могут учитываться выделенные, но не затронутые страницы (в системах с оптимистичным перераспределением, таких как Linux), страницы, которые выделены, но помечены MADV_FREE и т.д.

Если вы действительно измеряете выделенные страницы (что на самом деле не очень полезно, но, похоже, это то, о чем вы спрашиваете), и страницы действительно были освобождены, это может произойти при двух обстоятельствах: либо вы использовали brk или эквивалент для сокращения сегмента данных (что в настоящее время встречается очень редко), либо вы использовали munmap или что-то подобное для освобождения отображенного сегмента. (Теоретически также существует второстепенный вариант последнего, поскольку существуют способы освободить часть отображенного сегмента — например, украсть его с помощью MAP_FIXED для MADV_FREE сегмента, который вы немедленно отменяете.)

Но большинство программ напрямую не выделяют объекты из страниц памяти; они используют распределитель в malloc стиле. При вызове free распределитель может передавать страницы в ОС только в том случае, если вы просто случайно freeвводите последний активный объект в сопоставлении (или на последних N страницах сегмента данных). Ваше приложение никак не может разумно предсказать это или даже обнаружить, что это произошло заранее.

CPython делает это еще более сложным — у него есть пользовательский двухуровневый распределитель объектов поверх пользовательского распределителя памяти поверх malloc. (Смотрите Комментарии к источнику для более подробного объяснения.) И вдобавок ко всему, даже на уровне C API, не говоря уже о Python, вы даже напрямую не контролируете, когда освобождаются объекты верхнего уровня.

Итак, когда вы освобождаете объект, как вы узнаете, освободит ли он память для операционной системы? Что ж, сначала вы должны знать, что вы освободили последнюю ссылку (включая любые внутренние ссылки, о которых вы не знали), что позволяет GC освободить ее. (В отличие от других реализаций, по крайней мере, CPython освободит объект, как только это будет разрешено.) Обычно это освобождает как минимум две вещи на следующем уровне вниз (например, для строки вы освобождаете PyString объект и строковый буфер).

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

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

Если вы делаете free mallocредактируемую область, чтобы узнать, вызывает ли это munmap или эквивалент (or brk), вы должны знать внутреннее состояние malloc, а также то, как это реализовано. И этот вариант, в отличие от других, сильно зависит от платформы. (И опять же, обычно вам приходится освобождать последний используемый malloc в mmap сегменте, и даже тогда этого может не произойти.)

Итак, если вы хотите понять, почему получилось освободить ровно 50,5 МБ, вам придется проследить это снизу вверх. Почему malloc размонтировал страницы объемом 50,5 Мб при выполнении этих одного или нескольких free вызовов (вероятно, чуть больше 50,5 МБ)? Вам придется прочитать информацию о вашей платформе malloc, а затем пройтись по различным таблицам и спискам, чтобы увидеть ее текущее состояние. (На некоторых платформах может даже использоваться информация системного уровня, которую практически невозможно получить без создания моментального снимка системы для проверки в автономном режиме, но, к счастью, обычно это не проблема.) И затем вы должны сделать то же самое на 3 уровнях выше этого.

Итак, единственный полезный ответ на вопрос - "Потому что".

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

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

Ответ 4

Во-первых, вы можете захотеть установить glances:

sudo apt-get install python-pip build-essential python-dev lm-sensors 
sudo pip install psutil logutils bottle batinfo https://bitbucket.org/gleb_zhulik/py3sensors/get/tip.tar.gz zeroconf netifaces pymdstat influxdb elasticsearch potsdb statsd pystache docker-py pysnmp pika py-cpuinfo bernhard
sudo pip install glances

Затем запустите это в терминале!

glances

В вашем коде на Python добавьте в начале файла следующее:

import os
import gc # Garbage Collector

После использования "Большой" переменной (например: myBigVar), для которой вы хотели бы освободить память, напишите в своем коде python следующее:

del myBigVar
gc.collect()

В другом терминале запустите свой код на python и посмотрите в терминале "glances", как управляется память в вашей системе!

Удачи!

P.S. Я предполагаю, что вы работаете в системе Debian или Ubuntu

python