如何销毁 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中的显式del
s 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 对象并释放内存的主要内容,如果未能解决你的问题,请参考以下文章