Освобождение памяти в Python
У меня есть несколько связанных вопросов, касающихся использования памяти в следующем примере.
Если я запущу в интерпретаторе,
foo = ['bar' for _ in xrange(10000000)]
реальная память, используемая на моей машине, увеличивается до
80.9mb
. Затем я,del foo
реальная память сокращается, но только до
30.4mb
. Интерпретатор использует4.4mb
базовый уровень, так в чем преимущество отсутствия освобождения26mb
памяти для ОС? Это потому, что Python "планирует заранее", думая, что вы снова сможете использовать такой объем памяти?Почему это происходит, в частности,
50.5mb
- на основе какого объема освобождается память?Есть ли способ заставить 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 тыс. результатов, вы даже не заметите этого, но если вы отправляете и получаете большие объемы данных, вам захочется использовать какой-либо другой механизм (файл,
mmap
ped или иной; 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