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

15

我试图迭代遍历100,000张图片,并捕获一些图像特征并将结果数据框存储为pickle文件。不幸的是,由于RAM限制,我被迫将图像分成20,000个块,并在保存结果到磁盘之前对它们执行操作。下面编写的代码应该在开始处理下一个20,000个图像之前保存结果的数据帧。然而,这似乎没有解决我的问题,因为内存没有在第一个for循环结束时释放。因此,在处理第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对象。 - Space Impact
这篇帖子可能有助于找出要删除的对象,即您可以调用proc.get_memory_info()来比较GC之前和之后的内存使用情况。您可能也会无意中破坏堆,而Python GC可能会或可能不会为您进行碎片整理(即使您“删除并收集”那些死对象,也会导致内存使用量增加)。 - cronburg
1
不要使用线程来处理CPU密集型任务,而应该使用进程。无论如何,不要将并行任务的数量设置为计算机上的CPU数量。 - igrinis
@will 我正在调用一个 REST API 端点。这不是一个 CPU 绑定的任务。因此我正在使用线程。 - Thalish Sajeed
你也可以通过打印globals()中前10个最大的对象来进行“剖析”:var_sizes = {}; for var_name, var_value in globals().items(): var_sizes[var_name] = sys.getsizeof(var_value); [print(f"{var_name}: {size}") for (var_name, size) in sorted(var_sizes.items(), key=lambda k_v: k_v[1])[:10]]。但对于列表/字典/容器,您还应该添加存储项的大小(请参见https://dev59.com/9HRB5IYBdhLWcg3w-789#30316760了解详情)。 - imposeren
显示剩余6条评论
8个回答

6

现在,可能是第50,000个元素非常大,导致了OOM问题,为了测试这个想法,我首先会尝试:

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

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


好的,进入代码...

首先,在Python中迭代而不是将整个列表生成到内存中,你不需要显式使用list构造函数。

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

我觉得你可能在这里误用了线程池:

防止向线程池提交更多任务。一旦所有任务都完成,工作进程将退出。

这读起来像是close可能仍在运行某些内容,虽然我猜这是安全的,但它感觉有点不符合Python的风格,最好使用线程池的上下文管理器:

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

在Python中,明确使用del并不能保证释放内存,具体原因请参考这里

你应该在join之后/with语句结束之后进行内存回收操作:

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

你可以尝试将其分成较小的块,例如10,000个或更小的块!

敲打1

这里我建议使用SQL数据库而不是使用Pandas DataFrames和大型列表,你可以使用本地的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

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


谢谢Andy,我还没有机会尝试这些方法。我现在关闭悬赏,并在尝试这些方法后更新此评论。 - Thalish Sajeed

1

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

  • 你是否正在使用来自的ThreadPool()?在中,这并没有得到很好的记录,我宁愿使用ThreadPoolExecutor(还可以参见此处
  • 尝试调试每个循环结束时保存在内存中的对象,例如使用此解决方案,该解决方案依赖于sys.getsizeof()返回所有声明的globals()及其内存占用量的列表。
  • 还要调用del results(虽然这不应该太大,我猜)

1
你的问题在于你正在使用线程,而应该使用多进程(CPU绑定 vs 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")

因此,数据帧将被序列化并保存在每个进程内部,而不是在进程退出后。进程一旦退出就会清除内存,因此这应该可以使内存占用量保持较低。


0

我认为使用celery是可能的,因为有了它,你可以轻松地在Python中使用并发和并行。

处理图像似乎是幂等和原子性的,因此可以作为celery任务

你可以运行几个工作者来处理任务 - 处理图像。

此外,它还有内存泄漏的配置


问题是关于内存使用情况,而不是如何并行化任务。 - Will

0

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

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

0
不要调用list(),它会创建一个内存中的列表,其中包含从divide_chunks()返回的任何内容。这可能是您的内存问题发生的地方。
您不需要一次性将所有数据保存在内存中。只需逐个迭代文件名,这样所有数据就不会一次性保存在内存中。
请发布堆栈跟踪,以便我们获得更多信息。

不太可能。那只是将文件名列表分成较小的子列表。 - Will

0
简而言之,您无法在Python解释器中释放内存。最好的方法是使用多进程,因为每个进程可以单独处理内存。
垃圾收集器会“释放”内存,但不是您想要的上下文。可以在CPython源代码中探索页面和池的处理。这里还有一篇高级文章:https://realpython.com/python-memory-management/

GC会自动收集动态存储的数据。对于重复使用或静态值,您需要使用gc.collect(),例如内置类型int、char等。 - ASHu2

0

pd.DataFrame(...) 可能会在某些 Linux 构建中泄漏(请参见 GitHub 问题 和 "解决方法"),因此即使使用 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")

顺便说一句,如果任何单个数据框太大,这种解决方案将无法帮助。只有通过减少CHUNK_SIZE才能解决这个问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接