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

128

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

  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или без).

Джаред
источник
4
Стоит отметить, что такое поведение не характерно для Python. Обычно, когда процесс освобождает некоторую память, выделенную в куче, память не возвращается обратно в ОС, пока процесс не завершится.
NPE
Ваш вопрос задает несколько вопросов - некоторые из них дублируются, некоторые из них не подходят для SO, некоторые из них могут быть хорошими вопросами. Вы спрашиваете, не освобождает ли Python память, при каких именно обстоятельствах он может / не может, каков основной механизм, почему он был разработан таким образом, есть ли какие-либо обходные пути или что-то еще?
abarnert
2
@abarnert Я объединил похожие подвопросы. Чтобы ответить на ваши вопросы: я знаю, что Python освобождает часть памяти для ОС, но почему не всю ее, и почему она выделяет столько памяти. Если есть обстоятельства, при которых это невозможно, почему? Какие обходные пути тоже.
Джаред
@jww Я так не думаю. Этот вопрос действительно относился к тому, почему процесс интерпретатора никогда не освобождает память даже после полного сбора мусора с вызовами gc.collect.
Джаред

Ответы:

86

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

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

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

import os
import gc
import psutil

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

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

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

# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.get_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.

Эрик Сан
источник
Определяется использование memory_info()вместо get_memory_info()иx
Азиз Альто
Вы получаете 10 ^ 7 intс даже в Python 3, но каждая из них заменяет последнюю в переменной цикла, поэтому они не все существуют одновременно.
Дэвис Херринг
Я столкнулся с проблемой утечки памяти, и я думаю, причина в том, что вы здесь ответили. Но как я могу доказать свою догадку? Есть ли какой-нибудь инструмент, который может показать, что многие пулы повреждены, но используется только небольшой блок?
ruiruige1991
130

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

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

Нет, нет. Но есть простой обходной путь: дочерние процессы.

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

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

Во-первых, самый простой способ создать дочерний процесс - использовать concurrent.futures(или, для версии 3.1 и ранее, futuresbackport на 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доступными).
abarnert
источник
Действительно хороший трюк, хотя и не решающий проблемы :( Но мне он очень нравится
ddofborg
32

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

Почему именно он выпускает 50,5 МБ - исходя из какой суммы выделяется?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

abarnert
источник
2

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

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

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

del myBigVar
gc.collect()

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

Удачи!

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

de20ce
источник