我正在观察无法解释的内存使用情况。下面,我提供了我的实际代码的精简版,但仍然表现出此行为。该代码旨在完成以下任务:
读取1000行的文本文件。每行是一个句子。将这1000个句子拆分为4个生成器。将这些生成器传递到线程池,并以250个句子并行运行特征提取。
在我的实际代码中,我从整个文件的所有句子中累积了功能和标签。
现在出现了奇怪的事情:分配了内存,但是即使不累积这些值也不会再次释放!它与我认为的线程池有关。总共占用的内存量取决于为任何给定单词提取多少个特征。我在这里用range(100)
模拟。看看:
from sys import argv
from itertools import chain, islice
from multiprocessing import Pool
from math import ceil
# dummyfied feature extraction function
# the lengt of the range determines howmuch mamory is used up in total,
# eventhough the objects are never stored
def features_from_sentence(sentence):
return [{'some feature' 'some value'} for i in range(100)], ['some label' for i in range(100)]
# split iterable into generator of generators of length `size`
def chunks(iterable, size=10):
iterator = iter(iterable)
for first in iterator:
yield chain([first], islice(iterator, size - 1))
def features_from_sentence_meta(l):
return list(map (features_from_sentence, l))
def make_X_and_Y_sets(sentences, i):
print(f'start: {i}')
pool = Pool()
# split sentences into a generator of 4 generators
sentence_chunks = chunks(sentences, ceil(50000/4))
# results is a list containing the lists of pairs of X and Y of all chunks
results = map(lambda x : x[0], pool.map(features_from_sentence_meta, sentence_chunks))
X, Y = zip(*results)
print(f'end: {i}')
return X, Y
# reads file in chunks of `lines_per_chunk` lines
def line_chunks(textfile, lines_per_chunk=1000):
chunk = []
i = 0
with open(textfile, 'r') as textfile:
for line in textfile:
if not line.split(): continue
i+=1
chunk.append(line.strip())
if i == lines_per_chunk:
yield chunk
i = 0
chunk = []
yield chunk
textfile = argv[1]
for i, line_chunk in enumerate(line_chunks(textfile)):
# stop processing file after 10 chunks to demonstrate
# that memory stays occupied (check your system monitor)
if i == 10:
while True:
pass
X_chunk, Y_chunk = make_X_and_Y_sets(line_chunk, i)
我用来调试的文件有50000非空行,这就是为什么我在一个地方使用硬编码50000的原因。如果您想使用相同的文件,他是您的链接,方便您使用:
https://www.dropbox.com/s/v7nxb7vrrjim349/de_wiki_50000_lines?dl=0
现在,当您运行此脚本并打开系统监视器时,您将观察到内存已用完,并且使用情况一直持续到第10个块,在此我故意进入一个无休止的循环,以证明内存仍在使用,甚至虽然我从不存储任何东西。
您能告诉我为什么会这样吗?我似乎不知道应该如何使用多处理池。
答案 0 :(得分:3)
首先,让我们澄清一些误会-尽管事实证明,这实际上并不是首先探索的正确途径。
当您使用Python分配内存时,当然必须从操作系统中获取该内存。
但是,释放内存时,它很少会返回操作系统,直到最终退出。相反,它进入一个“空闲列表”,或者实际上是出于不同目的的多个级别的空闲列表。这意味着,下次需要内存时,Python便已将其放置在内存中,并且可以立即找到它,而无需与OS进行对话来分配更多内存。通常,这会使占用大量内存的程序更快。
但这还意味着,尤其是在现代64位操作系统上,请通过查看活动监视器/任务管理器/等来尝试了解您是否确实存在任何内存压力问题。几乎是无用的。
标准库中的tracemalloc
模块提供了低级工具,以查看内存使用情况到底发生了什么。在更高级别上,您可以使用类似memory_profiler
的东西(如果启用了tracemalloc
支持,这很重要)可以将这些信息与来自psutil
这样的来源的操作系统级别信息放在一起。弄清楚事情的发展方向。
但是,如果您没有看到任何实际的问题-您的系统没有进入交换地狱,您没有遇到任何MemoryError
异常,那么您的性能就不会遇到任何奇怪的问题线性上升到N,然后突然在N + 1处全部死,以此类推-通常,您一开始就不必理会这些。
如果您确实发现问题,那么幸运的是,您已经解决了一半。正如我在顶部提到的那样,您分配的大多数内存只有在最终退出后才返回到操作系统。但是,如果所有内存使用情况都发生在子进程中,并且这些子进程没有状态,则可以使它们退出并在需要时重新启动。
当然,这样做会提高性能,包括进程拆除和启动时间,以及必须重新开始的页面映射和缓存,以及要求OS重新分配内存等等。而且还存在复杂性成本–您不能只运行一个池并让它做它的事;您必须介入它的工作,并为您进行回收过程。
multiprocessing.Pool
类中没有内置的支持。
您当然可以构建自己的Pool
。如果您想花哨的话,可以查看multiprocessing
的源代码并进行操作。或者,您可以从Process
个对象列表和一对Queue
对列表中构建一个琐碎的池。或者,您可以直接使用Process
对象而无需抽象池。
内存问题的另一个原因是您的各个进程都很好,但是您的进程太多了。
事实上,这里似乎就是这种情况。
您可以在此函数中创建一个由4个工作人员组成的Pool
:
def make_X_and_Y_sets(sentences, i):
print(f'start: {i}')
pool = Pool()
# ...
…,然后为每个块调用此函数:
for i, line_chunk in enumerate(line_chunks(textfile)):
# ...
X_chunk, Y_chunk = make_X_and_Y_sets(line_chunk, i)
因此,最终每个块都有4个新进程。即使每个人的内存使用量都很低,一次也要拥有数百个这样的内存会加起来。
更不用说您可能因为数百个进程争用4个内核而严重损害了时间性能,因此您浪费了时间进行上下文切换和OS调度,而不是进行实际工作。
正如您在评论中指出的那样,此问题的解决方法是微不足道的:只需创建一个全局pool
即可,而不是为每个呼叫创建一个新的全局{1>}。
很抱歉,所有Columbo都在这里,但是……仅此而已……此代码在模块的顶层运行:
for i, line_chunk in enumerate(line_chunks(textfile)):
# ...
X_chunk, Y_chunk = make_X_and_Y_sets(line_chunk, i)
…这就是试图加速缓冲池和所有子任务的代码。但是该池中的每个子进程都需要import
这个模块,这意味着它们都将最终运行相同的代码,并旋转另一个池和一组额外的子任务。
大概是在Linux或macOS上运行的,默认startmethod
是fork
,这意味着multiprocessing
可以避免使用此import
,因此您不必有一个问题。但是,与其他启动方法一样,此代码基本上是一个占用您所有系统资源的前炸弹。其中包括spawn
,这是Windows上的默认启动方法。因此,如果任何人都有可能在Windows上运行此代码,则应将所有这些顶级代码置于if __name__ == '__main__':
保护中。