Звільнення пам’яті в 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 пулах KiB, класифікованих для розміщення розмірів у кратних 8 байт - до 256 байт (512 байт в 3.3). Самі басейни знаходяться на 256 KiB аренах, тому, якщо використовується лише один блок в одному пулі, вся арена 256 KiB не буде випущена. У 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%

Редагувати:

Я перейшов на вимірювання відносно розміру VM процесу, щоб усунути наслідки інших процесів у системі.

Час виконання C (наприклад, glibc, msvcrt) скорочує купу, коли суміжний вільний простір у верхній частині досягає постійного, динамічного або настроюваного порогу. За допомогою glibc ви можете налаштувати це за допомогою mallopt(M_TRIM_THRESHOLD). Враховуючи це, не дивно, якщо купа скорочується більше - навіть набагато більше - ніж той блок, який ви free.

У 3.x rangeне створюється список, тому тест, наведений вище, не створить 10 мільйонів intоб'єктів. Навіть якщо це було, intтип 3.x в основному є 2.x long, який не реалізує вільний список.


Використовуйте memory_info()замість get_memory_info()та xвизначено
Aziz Alto

Ви навіть отримуєте 10 ^ 7 intс навіть у Python 3, але кожен замінює останню в змінній циклу, щоб вони не існували одразу.
Девіс-оселедець

У мене зустрічається проблема витоку пам’яті, і я здогадуюсь, що це саме те, про що ви тут відповіли. Але як я можу довести свою здогадку? Чи є якийсь інструмент, який може показувати, що багато пулів є невловимими, але використовується лише невеликий блок?
ruiruige1991

130

Я здогадуюсь, питання, яке вас тут справді хвилює:

Чи є спосіб змусити Python звільнити всю використану пам'ять (якщо ви знаєте, що більше не будете використовувати стільки пам’яті)?

Ні, немає. Але є легке вирішення: дитячі процеси.

Якщо вам потрібно 500 Мб тимчасового сховища протягом 5 хвилин, але після цього вам потрібно запустити ще 2 години і більше не зачіпати стільки пам’яті, нерестовіруйте дочірній процес, щоб виконати об'ємну пам'ять. Коли дочірній процес проходить, пам'ять звільняється.

Це не зовсім банально і безкоштовно, але це досить легко і дешево, що, як правило, досить добре, щоб торгівля була вартий.

По-перше, найпростіший спосіб створити дочірній процес - це concurrent.futures(або, для 3.1 і раніше, futuresрезервний порт на PyPI):

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

Якщо вам потрібно трохи більше контролю, використовуйте multiprocessingмодуль.

Витрати:

  • Запуск процесів на деяких платформах, зокрема Windows, є повільним. Ми говоримо тут мілісекунди, а не хвилини, і якщо ви обертаєте одну дитину, щоб виконати 300 секунд роботи, ви цього навіть не помітите. Але це не безкоштовно.
  • Якщо велика кількість тимчасової пам'яті, яку ви використовуєте, дійсно велика , це може призвести до заміни вашої основної програми. Звичайно, ви довго заощаджуєте час, тому що якщо ця пам’ять навічно висіла, це в певний момент призведе до зміни. Але це може перетворити поступове сповільнення в дуже помітні затримки одноразового (і ранніх) в деяких випадках використання.
  • Надсилання великої кількості даних між процесами може бути повільним. Знову ж таки, якщо ви говорите про надсилання аргументів понад 2К та отримання 64К результатів, ви навіть цього не помітите, але якщо ви надсилаєте та отримуєте велику кількість даних, вам потрібно буде скористатися іншим механізмом (файл, mmapпед або інше; API спільної пам’яті в multiprocessingтощо; тощо).
  • Надсилання великої кількості даних між процесами означає, що дані мають бути вибірними (або, якщо ви вставляєте їх у файл або загальну пам'ять, struct-able або в ідеалі - ctypes).

Дійсно приємний трюк, хоча і не вирішує проблему :( Але мені це дуже подобається
ddofborg

32

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

Чому він випускає 50,5 Мб, зокрема - на яку суму виходить?

На чому ґрунтується це, зрештою, ціла низка збігів всередині Python malloc, які дуже важко передбачити.

По-перше, залежно від способу вимірювання пам'яті, ви можете лише вимірювати сторінки, фактично відображені в пам'яті. У цьому випадку, щоразу, коли сторінка буде замінена пейджером, пам'ять відображатиметься як "звільнена", навіть якщо вона не була звільнена.

Або ви можете вимірювати сторінки, що використовуються, які можуть не враховувати виділені, але ніколи не торкаються сторінки (у системах, які оптимістично перерозподіляють, як-от Linux), сторінки, які виділяються, але позначаються тегами MADV_FREEтощо.

Якщо ви дійсно вимірюєте виділені сторінки (що насправді не дуже корисна річ, але це, здається, саме про вас запитують), а сторінки справді розміщені, дві обставини, за яких це може статися: або ви ' Ви використовуєте brkабо еквівалентно для зменшення сегмента даних (сьогодні дуже рідко), або ви використовували munmapабо подібне, щоб випустити відображений сегмент. (Теоретично також існує незначний варіант для останнього, оскільки існує спосіб випустити частину відображеного сегмента, наприклад, вкрасти його MAP_FIXEDза MADV_FREEсегмент, який ви негайно скапіюєте.)

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

CPython робить це ще більш складним - він має спеціальний дворівневий розподільник об'єктів поверх спеціального розподільника пам'яті зверху malloc. ( Більш детальне пояснення див. У коментарях до джерела .) А крім того, навіть на рівні API API, набагато менше Python, ви навіть не керуєте безпосередньо, коли об'єкти верхнього рівня розміщуються.

Отже, коли ви випускаєте об'єкт, як ви дізнаєтесь, чи збирається випустити пам'ять в ОС? Ну, спочатку ви повинні знати, що ви випустили останню посилання (включаючи будь-які внутрішні посилання, про які ви не знали), що дозволяє GC розібратися з нею. (На відміну від інших реалізацій, принаймні CPython буде розміщувати об'єкт, як тільки це буде дозволено.) Зазвичай це розміщує принаймні дві речі на наступному рівні вниз (наприклад, для рядка ви випускаєте PyStringоб'єкт та буфер рядків ).

Якщо ви робите звільнити об'єкт, щоб дізнатися , чи викликає це наступний рівень вниз , щоб звільнити блок зберігання об'єкта, ви повинні знати внутрішній стан об'єкта розподільника, а також , як це реалізовано. (Очевидно, це не може статися, якщо ви не перестанете останню річ у блоці, і навіть тоді це може не статися.)

Якщо ви робите звільнити блок зберігання об'єкта, щоб дізнатися , чи викликає це freeвиклик, ви повинні знати внутрішній стан розподільника PyMem, а також , як це реалізовано. (Знову ж таки, вам доведеться розставити останній блок, який використовується, в межах mallocобласті ed, і навіть тоді це може не статися.)

Якщо ви робите free в mallocобласті Е.Д., щоб дізнатися , чи викликає чи це munmapабо еквівалент (або brk), ви повинні знати внутрішній стан malloc, а також , як це реалізовано. І цей, на відміну від інших, є дуже платформенним. (І знову ж таки, вам, як правило, доводиться розбирати останній вхід mallocв mmapсегмент, і навіть тоді це може не статися.)

Отже, якщо ви хочете зрозуміти, чому трапилося саме 50,5 Мб, вам доведеться простежити його знизу вгору. Чому mallocви знімали 50,5 Мб сторінок, коли ви робили ці один або кілька freeдзвінків (напевно, трохи більше 50,5 Мб)? Вам потрібно буде прочитати свої платформи malloc, а потім пройтися по різних таблицях і списках, щоб побачити її поточний стан. (На деяких платформах він може навіть використовувати інформацію на рівні системи, яку майже неможливо зафіксувати, не зробивши знімок системи для інспектування в режимі офлайн, але, на щастя, це зазвичай не проблема.) І тоді вам доведеться зробіть те ж саме на 3-х рівнях вище цього.

Отже, єдина корисна відповідь на питання - «Тому що».

Якщо ви не займаєтеся обмеженими ресурсами (наприклад, вбудованими) розробками, у вас немає підстав дбати про ці деталі.

А якщо будуть робити ресурси обмеженого розвитку, знаючи ці деталі марно; вам, як правило, потрібно виконати кінцевий пробіг навколо всіх цих рівнів, а саме mmapпам’яті, яка вам потрібна на рівні програми (можливо, з одним простим, добре зрозумілим, розподільником зони, що знаходиться між ними).


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()

В іншому терміналі запустіть свій пітон-код і спостерігайте за терміналом «погляд», як керується пам'яттю у вашій системі!

Удачі!

PS Я припускаю, що ви працюєте над системою Debian або Ubuntu

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.