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

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

  1. Если я бегу в переводчике,

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

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

    del foo
    

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

  2. Почему он выпускает 50.5mb в частности - на какой основе выпущено?

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

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

Ответ 1

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

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

Попробуй так, и скажи мне, что получишь. Здесь ссылка для psutil.

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%

Edit:

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

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

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

Ответ 2

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

Есть ли способ заставить 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 -able или идеально ctypes -able).

Ответ 3

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

Почему он выпускает 50.5mb в частности - какова сумма, выпущенная на основе?

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

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

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

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

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

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

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

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

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

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

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

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

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

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