用Python释放内存

2024-06-06 22:43:16 发布

您现在位置:Python中文网/ 问答频道 /正文

在下面的例子中,我有一些关于内存使用的相关问题。

  1. 如果我在翻译程序中运行

    foo = ['bar' for _ in xrange(10000000)]
    

    我的机器上使用的实际内存达到80.9mb。我那时

    del foo
    

    真正的内存会减少,但只会减少到30.4mb。解释器使用4.4mb基线,那么不向操作系统释放26mb内存有什么好处呢?是不是因为Python“提前计划”,认为您可能会再次使用那么多内存?

  2. 为什么它会释放50.5mb特别是-释放的数量是基于什么?

  3. 有没有办法强迫Python释放所有使用过的内存(如果您知道您不会再使用那么多内存的话)?

注意 这个问题不同于How can I explicitly free memory in Python? 因为这个问题主要处理的是,即使解释器通过垃圾收集释放了对象(使用gc.collect或不使用gc.collect)之后,内存使用量也会从基线增加。


Tags: 内存in机器forfoobarmb解释器
3条回答

我猜你真正关心的问题是:

Is there a way to force Python to release all the memory that was used (if you know you won't be using that much memory again)?

不,没有。但是有一个简单的解决方法:子进程。

如果您需要500MB的临时存储空间5分钟,但之后您需要再运行2个小时,并且再也不会接触到那么多内存,则生成一个子进程来执行内存密集型工作。当子进程消失时,内存被释放。

这不是完全的琐碎和免费,但它是相当容易和便宜的,这通常是足够好的贸易是值得的。

首先,创建子进程的最简单方法是使用^{}(或者,对于3.1及更早版本,使用PyPI上的^{}backport):

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
    result = executor.submit(func, *args, **kwargs).result()

如果需要更多的控制,请使用^{}模块。

费用包括:

  • 进程启动在某些平台上有点慢,特别是Windows。我们说的是毫秒,而不是分钟,如果你让一个孩子做300秒的工作,你甚至不会注意到。但这不是免费的。
  • 如果您使用的大量临时内存实际上是,那么这样做可能会导致主程序被换出。当然,从长远来看,你是在节省时间,因为如果内存永远挂在那里,就必须导致在某个时刻交换。但在某些用例中,这可能会将逐渐的缓慢变成非常明显的一次(和早期)延迟。
  • 在进程之间发送大量数据可能会很慢。同样,如果您要发送超过2K个参数并返回64K个结果,您甚至不会注意到它,但是如果您要发送和接收大量数据,则需要使用其他机制(文件、mmapped或其他;中的共享内存api等)。
  • 在进程之间发送大量数据意味着数据必须是可pickle的(或者,如果将它们保存在文件或共享内存中,则可以是struct-可pickle的,或者理想情况下是ctypes-可pickle的)。

埃里克森回答了问题1,我回答了问题3(原来的问题4),现在我们回答问题2:

Why does it release 50.5mb in particular - what is the amount that is released based on?

它的基础是,最终,Python和malloc内部的一系列巧合,这些巧合很难预测。

首先,取决于您如何测量内存,您可能只测量实际映射到内存中的页。在这种情况下,当页面被寻呼机调出时,内存将显示为“已释放”,即使它没有被释放。

或者,您可能正在测量正在使用的页面,这些页面可能计算也可能不计算已分配但从未接触过的页面(在优化过度分配的系统上,如linux)、已分配但已标记为MADV_FREE的页面等

如果您确实在测量已分配的页(这实际上不是一件非常有用的事情,但似乎正是您所要求的),并且页确实已被释放,那么在两种情况下可能会发生这种情况:要么您使用了brk,要么相当于缩小了数据段(现在非常罕见),或者你用munmap或者类似的方法来释放一个映射片段。(理论上,后者也有一个小的变体,因为有一些方法可以释放一个映射段的一部分,例如,用MAP_FIXED来窃取一个立即取消映射的MADV_FREE段。)

但是大多数程序不会直接从内存页中分配内容;它们使用malloc样式的分配器。调用free时,如果恰好free正在映射中的最后一个活动对象(或数据段的最后N个页面),分配器只能向操作系统释放页面。您的应用程序无法合理地预测这一情况,甚至无法检测到它是提前发生的。

CPython使这变得更加复杂,它在malloc上的自定义内存分配器之上有一个自定义的2级对象分配器。(请参见the source comments以获得更详细的解释)除此之外,即使在C API级别,更不用说Python,您甚至无法直接控制顶级对象何时被释放。

所以,当你释放一个对象时,你如何知道它是否会向操作系统释放内存?好吧,首先你必须知道你已经发布了最后一个引用(包括你不知道的任何内部引用),允许GC释放它。(与其他实现不同的是,至少CPython会在允许的情况下尽快释放一个对象。)这通常会在下一级释放至少两个对象(例如,对于字符串,您释放的是PyString对象和字符串缓冲区)。

如果您要解除分配一个对象,要知道这是否会导致下一个级别降低以解除分配一个对象存储块,您必须知道对象分配器的内部状态,以及它是如何实现的。(很明显,除非你正在释放块中的最后一个东西,否则它是不可能发生的,即使这样,它也可能不会发生。)

如果您要释放一个对象存储块,要知道这是否会导致一个free调用,您必须知道PyMem分配器的内部状态,以及它是如何实现的。(同样,您必须在malloced区域内释放最后一个正在使用的块,即使这样,也可能不会发生。)

如果你要知道这是否会导致一个munmap或等效的(或brk)区域,你必须知道malloc的内部状态,以及它是如何实现的。而这一个,与其他的不同,是高度特定于平台的。(而且,通常必须在mmap段中释放最后一个正在使用的malloc,即使这样,也可能不会发生。)

所以,如果你想知道为什么它会发布50.5mb,你必须从下往上追踪它。当您进行一个或多个free调用(可能超过50.5mb)时,为什么要malloc取消映射值为50.5mb的页面?您必须阅读平台的malloc,然后n遍历各个表和列表以查看其当前状态。(在某些平台上,它甚至可能利用系统级信息,如果不制作系统快照进行脱机检查,几乎不可能捕获这些信息,但幸运的是,这通常不是问题。)然后,您必须在上述3个级别上执行相同的操作。

所以,唯一有用的答案就是“因为”

除非您正在进行资源有限(例如,嵌入式)开发,否则您没有理由关心这些细节。

如果您正在进行资源有限的开发,那么了解这些细节是没有用的;您几乎必须在所有这些级别上执行一次结束运行,特别是在应用程序级别上需要的内存(可能中间有一个简单的、很好理解的、特定于应用程序的区域分配器)。

堆上分配的内存可能会受到高水位线的影响。Python对4个KiB池中分配小对象(PyObject_Malloc)的内部优化使这一点变得复杂,这些KiB池的分配大小是8字节的倍数——高达256字节(3.3中为512字节)。池本身位于256个KiB竞技场中,因此如果仅使用一个池中的一个块,则不会释放整个256个KiB竞技场。在Python3.3中,小对象分配器被切换到使用匿名内存映射而不是堆,因此它在释放内存方面应该表现得更好。

此外,内置类型维护以前分配的对象的自由列表,这些对象可能使用也可能不使用小对象分配器。int类型使用自己分配的内存维护一个freelist,清除它需要调用PyInt_ClearFreeList()。这可以通过执行完整的gc.collect来间接调用。

像这样试试,告诉我你得到了什么。这是psutil.Process.memory_info的链接。

import os
import gc
import psutil

proc = psutil.Process(os.getpid())
gc.collect()
mem0 = proc.get_memory_info().rss

# create approx. 10**7 int objects and pointers
foo = ['abc' for x in range(10**7)]
mem1 = proc.get_memory_info().rss

# unreference, including x == 9999999
del foo, x
mem2 = proc.get_memory_info().rss

# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.get_memory_info().rss

pd = lambda x2, x1: 100.0 * (x2 - x1) / mem0
print "Allocation: %0.2f%%" % pd(mem1, mem0)
print "Unreference: %0.2f%%" % pd(mem2, mem1)
print "Collect: %0.2f%%" % pd(mem3, mem2)
print "Overall: %0.2f%%" % pd(mem3, mem0)

输出:

Allocation: 3034.36%
Unreference: -752.39%
Collect: -2279.74%
Overall: 2.23%

编辑:

我切换到相对于进程VM大小的测量,以消除系统中其他进程的影响。

当顶部的连续可用空间达到常量、动态或可配置阈值时,C运行时(例如glibc、msvcrt)会收缩堆。使用glibc,您可以使用^{}(M_TRIM_THRESHOLD)对其进行优化。考虑到这一点,如果堆比您free的块收缩得更多——甚至更多——也就不足为奇了。

在3.x中range不会创建列表,因此上面的测试不会创建1000万个int对象。即使是这样,3.x中的int类型基本上是一个2.x long,它没有实现自由列表。

相关问题 更多 >