Утечки памяти, когда изображение отбрасывается в Python

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

Когда изображения перезагружаются, все ссылки переносятся на новые данные изображения, поскольку они привязаны к той же переменной, к которой были привязаны исходные данные изображения. Я попытался форсировать сборку мусора с помощью collect(), но это не помогло.

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

from tkinter import Button, DISABLED, Frame, Label, NORMAL, Tk
from PIL.Image import open
from PIL.ImageTk import PhotoImage

class App(Tk):
    def __init__(self):
        Tk.__init__(self)
        self.text = Label(self, text = "Please check the memory usage. Then push button #1.")
        self.text.pack()
        self.btn = Button(text = "#1", command = lambda : self.buttonPushed(1))
        self.btn.pack()

    def buttonPushed(self, n):
        "Cycle to open the Tab module n times."
        self.btn.configure(state = DISABLED) # disable to prevent paralell cycles
        if n == 100:
            self.text.configure(text = "Overwriting the bitmap with itself 100 times...\n\nCheck the memory usage!\n\nUI may seem to hang but it will finish soon.")
            self.update_idletasks()
        for i in range(n):      # creates the Tab frame whith the img, destroys it, then recreates them to overwrite the previous Frame and prevous img
            b = Tab(self)
            b.destroy()
            if n == 100:
                print(i+1,"percent of processing finished.")
        if n == 1:
            self.text.configure(text = "Please check the memory usage now.\nMost of the difference is caused by the bitmap opened.\nNow push button #100.")
            self.btn.configure(text = "#100", command = lambda : self.buttonPushed(100))
        self.btn.configure(state = NORMAL)  # starting cycles is enabled again       

class Tab(Frame):
    """Creates a frame with a picture in it."""
    def __init__(self, master):
        Frame.__init__(self, master = master)
        self.a = PhotoImage(open("map.png"))    # img opened, change this to a valid one to test it
        self.b = Label(self, image = self.a)
        self.b.pack()                           # Label with img appears in Frame
        self.pack()                             # Frame appears

if __name__ == '__main__':
    a = App()

Для запуска приведенного выше кода вам понадобится файл изображения PNG. Размер моего файла map.png составляет 1062×1062. В формате PNG это 1,51 МБ, а в виде растровых данных - около 3-3,5 МБ. Используйте большое изображение, чтобы легко увидеть утечку памяти.

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

Пожалуйста, дайте мне несколько советов, как решить эту проблему. Я благодарен за каждую помощь. Спасибо. заблаговременно.


person bardosd    schedule 27.06.2013    source источник
comment
Во-первых, действительно ли проблема потреблять 500 МБ? Если на то пошло, это 500 МБ просто виртуальная память или физическая/резидентная память? Python обычно не возвращает память ОС; он хранит его для повторного использования, когда он вам понадобится позже. И это обычно ускоряет работу — выделение, освобождение и перераспределение десятков МБ снова и снова занимает много времени. Кроме того, на какой платформе вы находитесь? Например, в 64-битной OS X большинство процессов заканчиваются сотнями МБ виртуальной машины, тогда как в 32-битной Linux это встречается гораздо реже.   -  person abarnert    schedule 27.06.2013
comment
Я не знаю, было ли это физическое или VRAM. Я новичок в программировании и не знаю инструмента, чтобы это проверить. Не могли бы вы порекомендовать некоторые? Я использовал список задач из командной строки и диспетчер задач, чтобы отслеживать потребление памяти. Моя ОС Win7 x64. Так вы говорите, что это не проблема, пока он иногда рушится? Это было бы большим облегчением.   -  person bardosd    schedule 27.06.2013
comment
TaskManager показывает отдельные числа для физической и виртуальной памяти, но я не могу точно вспомнить, как они называются. В любом случае, если вы считаете, что проблема может быть реальной, вам нужно узнать, как работает управление памятью в Windows, прежде чем вы сможете даже исследовать ее. Если у вас нет реальной проблемы, и вы просто беспокоитесь и не знаете, почему, просто перестаньте беспокоиться.   -  person abarnert    schedule 27.06.2013


Ответы (2)


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


И я предполагаю, что у вас вообще нет никаких проблем.

Когда сборщик мусора Python убирает все (что обычно происходит сразу после того, как вы закончите с этим в CPython), он, как правило, фактически не освобождает память для ОС. Вместо этого он хранит его на случай, если он понадобится вам позже. Это сделано намеренно — если только вы не перегружаете своп, гораздо быстрее повторно использовать память, чем постоянно ее освобождать и перераспределять.

Кроме того, если 500 МБ — это виртуальная память, это ничто на современной 64-битной платформе. Если она не отображается в физическую/резидентную память (или отображается, если компьютер бездействует, но в противном случае быстро выбрасывается), это не проблема; просто ОС хороша с ресурсами, которые фактически бесплатны.

Что еще более важно: что заставляет вас думать, что есть проблема? Есть ли какой-то реальный симптом или просто что-то в диспетчере программ/мониторе активности/вверху/что-то еще, что вас пугает? (Если последнее, взгляните на другие программы. На моем Mac у меня есть 28 программ, которые в настоящее время работают, используя более 400 МБ виртуальной памяти, и я использую 11 из 16 ГБ, хотя меньше 3 ГБ). Если я, скажем, запущу Logic, память будет собрана быстрее, чем Logic сможет ее использовать, а до тех пор, почему ОС должна тратить усилия на размаппинг памяти (особенно если у нее нет возможности убедиться, что некоторые процессы не будут пойти попросить ту память, которую он не использовал позже)?


Но если существует реальная проблема, есть два способа ее решения.


Первая хитрость заключается в том, чтобы сделать все, что интенсивно использует память, в дочернем процессе, который вы можете убить и перезапустить, чтобы восстановить временную память (например, с помощью multiprocessing.Process или concurrent.futures.ProcessPoolExecutor).

Обычно это замедляет работу, а не ускоряет ее. И это, очевидно, нелегко сделать, когда временная память — это в основном вещи, которые идут прямо в GUI и, следовательно, должны жить в основном процессе.


Другой вариант — выяснить, где используется память, и не держать так много объектов одновременно. По сути, здесь есть две части:

Во-первых, выпускайте все возможное до окончания каждого обработчика событий. Это означает вызов close для файлов, либо delобъекты, либо установку всех ссылок на них в None, вызов destroy для объектов GUI, которые не видны, и, самое главное, не сохранение ссылок на вещи, которые вам не нужны. (Вам действительно нужно хранить PhotoImage после того, как вы его используете? Если да, есть ли способ загрузить изображения по запросу?)

Затем убедитесь, что у вас нет эталонных циклов. В CPython мусор очищается немедленно, пока нет циклов, но если они есть, они остаются без дела, пока не запустится средство проверки циклов. Вы можете использовать gc модуль, чтобы исследовать это. Одна очень быстрая вещь, которую можно сделать, это попробовать это время от времени:

print(gc.get_count())
gc.collect()
print(gc.get_count())

Если вы видите огромные капли, у вас есть циклы. Вам придется заглянуть внутрь gc.getobjects() и gc.garbage, или прикрепить обратные вызовы, или просто подумать о своем коде, чтобы точно определить, где находятся циклы. Для каждого из них, если вам действительно не нужны ссылки в обоих направлениях, избавьтесь от одного; если вы это сделаете, измените один из них на weakref.

person abarnert    schedule 27.06.2013
comment
Спасибо за ваш ответ. Я волновался, потому что обращаю внимание на то, чтобы использовать как можно меньше ресурсов, но все же он только рос и рос. Теперь я вижу, что это нормально. Я также благодарен за ваши подсказки по решению проблем. Они пригодятся, если у меня будут настоящие проблемы с памятью. :) - person bardosd; 27.06.2013

Экономия 500 МБ стоит, экономия 100 МБ стоит, экономия 10 МБ стоит. Память имеет цену золота и многие предлагают ее растратить. Определенно, это ваше решение, если вы хотите потратить его на свой Mac, сделайте это... И абсолютно, это очень печальный совет, как писать очень плохое программное обеспечение.

Используйте https://pypi.org/project/memory-profiler/ для отслеживания вашего Python выделения памяти. Использовать

x = someRamConsumingObject()
# do the stuff here ...
# remove the refrences
del x
x = None
gc.Collect() # try to force garbage collector to collect

Помимо философских дискуссий, реальные примеры из промышленных граничных вычислений дают нам точные причины, по которым это нужно улучшить. Если вы запускаете Python в контейнерах, вы скоро упретесь в стену, особенно если несколько контейнеров работают на Edge под большой производственной нагрузкой.

И даже если у Edge есть 16 ГБ, вы скоро упретесь в стену, особенно используя инструменты анализа данных, такие как Pandas.

Тогда, мой друг, ты поймешь, что такое адские сборщики мусора и что значит не иметь памяти под контролем.

С++ рулит!!!

person OSP    schedule 16.09.2020