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

Posted

技术标签:

【中文标题】如何销毁 Python 对象并释放内存【英文标题】:How to destroy Python objects and free up memory 【发布时间】:2019-10-01 05:22:04 【问题描述】:

我正在尝试迭代超过 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")

【问题讨论】:

我认为在 python 中,我们没有释放内存的能力。但是我们可以使用del 命令删除一个python对象。 从代码中你可以看到我使用了 del 并且还调用了垃圾收集器,但它的行为似乎不像你描述的那样 This post 可能有助于确定要删除的对象,即您可以调用 proc.get_memory_info() 来比较 GC 前后的内存使用情况。您也可能在不知不觉中对堆进行了碎片整理,python GC 可能会或可能不会为您进行碎片整理(即使您“删除并收集”那些死对象,也会导致内存使用量增加)。 不要将线程用于 CPU 密集型任务,而应使用进程。无论如何,不​​要将并行任务的数量设置为超过计算机上的 CPU 数量。 get_image_features 内部发生了什么?您在 sn-p 中所做的一切都很好。 【参考方案1】:

现在,可能是第 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)

我认为您可能在这里误用了 ThreadPool:

防止任何更多的任务被提交到池中。完成所有任务后,工作进程将退出。

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

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

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

你应该收集 加入/在 with 之后:

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

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


锤子 1

我会考虑在这里做的一件事,而不是使用 pandas DataFrames 和大型列表是使用 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))

然后,计算完成后,您可以从数据库中选择所有您喜欢的格式。例如。使用read_sql。


锤子 2

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

由于您可以将 start 和 end 作为 sys.args 传递给 python,因此您可以对这些进行切片:

# 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(或其他一些数据库)完全可以避免这种情况。

【讨论】:

谢谢安迪,我还没有机会尝试这些方法。我现在正在关闭赏金,一旦我有机会尝试这些方法,我会更新此评论。【参考方案2】:

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

你在使用ThreadPool()from multiprocessing.pool吗?这并没有很好的记录(在python3),我宁愿使用ThreadPoolExecutor,(另见here) 尝试在每个循环的最后调试哪些对象保存在内存中,例如使用依赖于sys.getsizeof() 的this solution 返回所有声明的globals() 的列表,以及它们的内存占用。 也调用del results(虽然我猜应该不会太大)

【讨论】:

【参考方案3】:

您的问题是您正在使用应该使用多处理的线程(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")

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

【讨论】:

【参考方案4】:

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

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

请发布堆栈跟踪,以便我们了解更多信息

【讨论】:

怀疑。这只是将文件名列表划分为更小的子列表。【参考方案5】:

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

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

【讨论】:

GC 自动收集动态存储的数据。对于重用或静态值,您需要gc.collect(),例如 int、char 等内置类型。【参考方案6】:

我认为celery 将成为可能,感谢 celery,您可以轻松地使用 python 使用并发性和并行性。

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

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

此外,它还有configuration 用于内存泄漏。

【讨论】:

问题是关于内存使用而不是关于如何并行化任务。【参考方案7】:

我对这类问题的解决方案是使用一些并行处理工具。我更喜欢joblib,因为它甚至允许并行化本地创建的函数(这是“实现细节”,因此最好避免在模块中将它们设为全局)。我的另一个建议:不要在 python 中使用线程(和线程池),而是使用进程(和进程池)——这几乎总是一个更好的主意!只需确保在 joblib 中创建至少 2 个进程的池,否则它将运行原始 python 进程中的所有内容,因此最终不会释放 RAM。一旦 joblib 工作进程自动关闭,它们分配的 RAM 将被操作系统完全释放。我最喜欢的武器是joblib.Parallel。如果您需要向worker传输大数据(即大于2GB),请使用joblib.dump(在主进程中将python对象写入文件)和joblib.load(在worker进程中读取)。

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

【讨论】:

【参考方案8】:

pd.DataFrame(...) 可能会在某些 linux 版本上泄漏(请参阅 github issue and "workaround"),因此即使 del df 也可能无济于事。

在您的情况下,可以使用来自 github 的解决方案,而无需对 pd.DataFrame.__del__ 进行猴子修补:

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

附:如果任何单个数据框太大,此解决方案将无济于事。这只能通过减少CHUNK_SIZE来帮助

【讨论】:

以上是关于如何销毁 Python 对象并释放内存的主要内容,如果未能解决你的问题,请参考以下文章

python如何让程序一直运行且内存资源自动释放?

java基础:对象的销毁

JavaScript内存释放和作用域销毁

JavaScript基础知识六(内存释放作用域销毁)

js的内存释放初步理解

对象的引用,释放和销毁