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

15 人关注

我试图在100,000张图片上进行迭代,捕捉一些图片特征,并将得到的dataFrame作为一个pickle文件存储在磁盘上。

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

下面写的代码应该是在开始循环处理下一个20,000张图片之前保存20,000张图片的结果数据框。

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

因此,在处理第50000条记录的某个地方,程序因内存不足而崩溃。

我试着在将对象保存到磁盘并调用垃圾收集器后将其删除,然而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")
    
5 个评论
我认为在python中,我们没有释放内存的能力。但是我们可以使用 del 命令删除一个Python对象。
从代码中,你可以看到我使用了del,也调用了垃圾收集器,但似乎并没有像你描述的那样表现出来
This post 可以帮助找出要删除的对象,也就是说,你可以调用 proc.get_memory_info() 来比较GC前后的内存使用情况。你也可能在不知不觉中分割了你的堆,Python GC可能会也可能不会为你进行碎片整理(导致内存使用量增加,即使你 "删除并收集 "了这些死对象)。
不要对CPU密集型任务使用线程,而应使用进程。总之,不要把并行任务的数量设置得超过你电脑上的CPU数量。
Will
get_image_features 里面发生了什么?你在你的代码段中所做的是很好的。
python
pandas
memory-management
out-of-memory
Thalish Sajeed
Thalish Sajeed
发布于 2019-05-14
8 个回答
Andy Hayden
Andy Hayden
发布于 2019-05-25
已采纳
0 人赞同

现在,可能是第5万次中的某些东西非常大,导致了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可能有一些认为还在运行,虽然我猜这是安全的,但感觉有点不靠谱,最好使用ThreadPool的上下文管理器。

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

The explicit dels in python 实际上并不能保证释放内存.

You should collect 之后 the join/之后 the with:

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

你也可以尝试将其分成更小的部分,例如10,000或更小的部分。

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

Then, 之后 the calculation is complete you can select all from the database, into which ever format you like. E.g. using 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]):
    # do stuff

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

我敢打赌,Hammer 1才是正确的选择,感觉你在胶着大量的数据,并不必要地将其读入python列表,而使用sqlite3(或其他数据库)完全避免了这一点。

谢谢你,安迪,我还没有机会尝试这些方法。我现在正在关闭赏金,一旦我有机会尝试这些方法,我将更新这个评论。
Asmus
Asmus
发布于 2019-05-25
0 人赞同

Note: this is not an answer, rather a quick list of questions & suggestions

  • Are you using ThreadPool() from multiprocessing.pool ? That isn't really well documented (in python3 ) and I'd rather use ThreadPoolExecutor , (also see here )
  • try to debug which objects are held in memory at the very end of each loop, e.g. using this solution which relies on sys.getsizeof() to return a list of all declared globals() , together with their memory footprint.
  • also call del results (although that shouldn't be to large, I guess)
  • delica
    delica
    发布于 2019-05-25
    0 人赞同

    你的问题是,你在应该使用多处理的地方使用了线程(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")
    

    因此,数据框架将被提取并保存在每个进程中,而不是在它退出后。进程一旦退出就会被清理出内存,所以这应该可以保持较低的内存占用率。

    0 人赞同

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

    你不需要一下子把所有的数据都放在内存中。 只要一次一次地迭代文件名就可以了,这样所有的数据就不会一下子出现在内存中。

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

    Will
    怀疑它。这只是把一个文件名的列表分成更小的子列表。
    user397836
    user397836
    发布于 2019-05-25
    0 人赞同

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

    垃圾收集器会 "释放 "内存,但不是你所期望的那种情况。对页和池的处理可以在CPython源代码中进行探讨。这里也有一篇高水平的文章。 https://realpython.com/python-memory-management/

    GC会自动收集动态存储的数据。对于重复使用或静态的值,你需要 gc.collect() ,比如int、char等内置类型。
    MartinP
    MartinP
    发布于 2019-05-25
    0 人赞同

    I think it will be possible with celery 由于celery的存在,你可以用python轻松地使用并发性和并行性。

    处理图像似乎是闲置的和原子性的,所以它可以是一个 芹菜任务 .

    你可以运行 几个工人 这将处理任务--与图像一起工作。

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

    Will
    问题是关于内存的使用,而不是关于如何将任务并行化。
    S.V
    S.V
    发布于 2019-05-25
    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进程,你绝对无法从python内部释放它,在这种情况下,它将保证被操作系统释放。这就是为什么 在Python中释放内存的唯一100%可靠的方法是在一个并行进程中运行分配内存的代码,然后终止该进程。 .

    imposeren
    imposeren
    发布于 2019-05-25
    0 人赞同

    替换代码0】在某些Linux构建中可能会泄漏(见github 问题和 "变通 "办法 ), so even del df might not help.

    在你的情况下,可以使用github的解决方案,而不需要对 pd.DataFrame.__del__ 进行猴子式的修补。

    from ctypes import cdll, CDLL
        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)