如何销毁 Python 个对象并释放内存

How to destroy Python objects and free up memory

我正在尝试迭代超过 100,000 张图像并捕获一些图像特征并将生成的 dataFrame 作为 pickle 文件存储在磁盘上。

不幸的是,由于 RAM 的限制,我不得不将图像分成 20,000 个块,并在将结果保存到磁盘之前对它们执行操作。

下面编写的代码应该在开始循环处理接下来的 20,000 张图像之前保存 20,000 张图像的结果数据帧。

但是 - 这似乎没有解决我的问题,因为在第一个 for 循环结束时内存没有从 RAM 中释放

因此在处理第 50,000 条记录时,程序因内存不足错误而崩溃。

我尝试在将对象保存到磁盘并调用垃圾收集器后将其删除,但 RAM 使用率似乎没有下降。

我错过了什么?

#file_list_1 contains 100,000 images
file_list_chunks = list(divide_chunks(file_list_1,20000))
for count,f in enumerate(file_list_chunks):
    # make the Pool of workers
    pool = ThreadPool(64) 
    results = pool.map(get_image_features,f)
    # close the pool and wait for the work to finish 
    list_a, list_b = zip(*results)
    df = pd.DataFrame({'filename':list_a,'image_features':list_b})
    df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")
    del list_a
    del list_b
    del df
    gc.collect()
    pool.close() 
    pool.join()
    print("pool closed")

注意:这不是答案,而是问题和建议的快速列表

  • 您在使用 ThreadPool() from multiprocessing.pool 吗?这并没有很好的记录(在 python3 中),我宁愿使用 ThreadPoolExecutor, (also see here)
  • 尝试调试在每个循环结束时哪些对象保存在内存中,例如使用依赖于 sys.getsizeof() 到 return 所有已声明 globals() 的列表及其内存占用。
  • 也调用 del results(虽然我想应该不会太大)

不要调用 list(),它正在创建一个 in-memory 从 divide_chunks() 返回的任何内容的列表。 这就是您的内存问题可能发生的地方。

您不需要一次将所有数据存储在内存中。 一次只遍历一个文件名,这样所有数据就不会同时在内存中。

请post堆栈跟踪以便我们有更多信息

简而言之,您无法在 Python 解释器中释放内存。最好的选择是使用多处理,因为每个进程都可以自己处理内存。

垃圾收集器将 "free" 内存,但不是您预期的上下文。可以在 CPython 源代码中探索页面和池的处理。这里还有一篇高水平的文章:https://realpython.com/python-memory-management/

我认为使用 celery 是可能的,多亏了 celery,您可以使用 python.

轻松使用并发性和并行性

处理图像似乎是幂等的和原子的,所以它可以是 celery task

您可以 运行 a few workers 处理任务 - 使用图像。

此外它还有 configuration 内存泄漏。

我解决这类问题的方法是使用一些并行处理工具。我更喜欢joblib since it allows to parallelize even locally created functions (which are "details of implementation" and so it is better to avoid making them global in a module). My other advise: do not use threads (and thread pools) in python, use processes (and process pools) instead - this is almost always a better idea! Just make sure to create a pool of at least 2 processes in joblib, otherwise it would run everything in the original python process and so RAM would not be released in the end. Once the joblib worker processes are closed automatically, RAM which they allocated will be fully released by the OS. My favorite weapon of choice is joblib.Parallel. If you need to transfer to workers large data (i.e. larger than 2GB), use joblib.dump (to write a python object into a file in the main process) and joblib.load(在工作进程中阅读)。

关于del object:在python中,该命令实际上并没有删除一个对象。它只会减少其引用计数器。当您 运行 import gc; gc.collect() 时,垃圾收集器会自行决定释放哪些内存以及分配哪些内存,我不知道有什么方法可以强制它释放所有可能的内存。更糟糕的是,如果某些内存实际上不是由 python 分配的,而是在某些外部 C/C++/Cython/etc 代码中分配的,并且代码没有关联 python 引用计数器与内存,除了我上面写的,你绝对无法从 python 中释放它,即终止分配 RAM 的 python 进程,在这种情况下它将保证被 OS 释放。这就是为什么 在 python 中释放一些内存的唯一 100% 可靠的方法是 运行 在并行进程中分配它的代码,然后终止进程 .

现在,可能是第 50,000 个中的某些东西非常大,这导致了 OOM,所以为了测试这个我首先尝试:

file_list_chunks = list(divide_chunks(file_list_1,20000))[30000:]

如果它在 10,000 时失败,这将确认 20k 是否太大了块大小,或者如果它在 50,000 时再次失败,则代码有问题...


好的,进入代码...

首先,您不需要显式 list 构造函数,在 python 中迭代而不是将整个列表生成到内存中要好得多。

file_list_chunks = list(divide_chunks(file_list_1,20000))
# becomes
file_list_chunks = divide_chunks(file_list_1,20000)

我认为您可能在此处误用了线程池:

Prevents any more tasks from being submitted to the pool. Once all the tasks have been completed the worker processes will exit.

这看起来像 close 可能还有一些想法 运行,虽然我猜这是安全的,但感觉有点 un-pythonic,最好使用 ThreadPool 的上下文管理器:

with ThreadPool(64) as pool: 
    results = pool.map(get_image_features,f)
    # etc.

python aren't actually guaranteed to free memory 中的显式 del

你应该 join/after 之后收集:

with ThreadPool(..):
    ...
    pool.join()
gc.collect()

您也可以尝试将其分成更小的部分,例如10,000 甚至更少!


锤子 1

有一件事,我会考虑在这里做,而不是使用 pandas DataFrame 和大列表是使用 SQL 数据库,你可以在本地使用 sqlite3:

import sqlite3
conn = sqlite3.connect(':memory:', check_same_thread=False)  # or, use a file e.g. 'image-features.db'

并使用上下文管理器:

with conn:
    conn.execute('''CREATE TABLE images
                    (filename text, features text)''')

with conn:
    # Insert a row of data
    conn.execute("INSERT INTO images VALUES ('my-image.png','feature1,feature2')")

这样,我们就不必处​​理大型列表对象或 DataFrame。

您可以将连接传递给每个线程...您可能需要一些奇怪的东西,例如:

results = pool.map(get_image_features, zip(itertools.repeat(conn), f))

然后,在计算完成后,您可以 select 全部从数据库中,转换成您喜欢的任何格式。例如。使用 read_sql.


锤子 2

在这里使用子进程,而不是 运行 在 python "shell out" 的同一实例中使用另一个。

由于您可以将开始和结束传递给 python 作为 sys.args,您可以将这些切片:

# main.py
# a for loop to iterate over this
subprocess.check_call(["python", "chunk.py", "0", "20000"])

# chunk.py a b
for count,f in enumerate(file_list_chunks):
    if count < int(sys.argv[1]) or count > int(sys.argv[2]):
         pass
    # do stuff

这样,子进程将正确清理 python(不会有内存泄漏,因为进程将被终止)。


我敢打赌,Hammer 1 是必经之路,感觉就像您在粘合大量数据,并不必要地将其读入 python 列表,并使用 sqlite3(或其他一些数据库) ) 完全避免了。

您的问题是您在应该使用多处理的地方使用线程(CPU 绑定与 IO 绑定)。

我会像这样重构您的代码:

from multiprocessing import Pool

if __name__ == '__main__':
    cpus = multiprocessing.cpu_count()        
    with Pool(cpus-1) as p:
        p.map(get_image_features, file_list_1)

然后我会通过将这两行附加(类似)到它的末尾来更改函数 get_image_features。我不知道你是如何处理这些图像的,但我的想法是在每个进程中处理每个图像,然后立即将它保存到磁盘:

df = pd.DataFrame({'filename':list_a,'image_features':list_b})
df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")

所以数据帧将在每​​个进程中被 pickle 并保存,而不是在它退出之后。进程一退出就会从内存中清除,因此这应该可以保持较低的内存占用。

pd.DataFrame(...) 可能会在某些 linux 构建中泄漏(参见 github issue and "workaround"),因此即使 del df 也可能无济于事。

在您的情况下,github 的解决方案可以在没有 pd.DataFrame.__del__ 的 monkey-patching 的情况下使用:

from ctypes import cdll, CDLL
try:
    cdll.LoadLibrary("libc.so.6")
    libc = CDLL("libc.so.6")
    libc.malloc_trim(0)
except (OSError, AttributeError):
    libc = None


if no libc:
    print("Sorry, but pandas.DataFrame may leak over time even if it's instances are deleted...")


CHUNK_SIZE = 20000


#file_list_1 contains 100,000 images
with ThreadPool(64) as pool:
    for count,f in enumerate(divide_chunks(file_list_1, CHUNK_SIZE)):
        # make the Pool of workers
        results = pool.map(get_image_features,f)
        # close the pool and wait for the work to finish 
        list_a, list_b = zip(*results)
        df = pd.DataFrame({'filename':list_a,'image_features':list_b})
        df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")

        del df

        # 2 new lines of code:
        if libc:  # Fix leaking of pd.DataFrame(...)
            libc.malloc_trim(0)

print("pool closed")

P.S。如果任何单个数据框太大,此解决方案将无济于事。这只能通过减少 CHUNK_SIZE

来帮助