【问题标题】:Understanding memory usage in python了解python中的内存使用情况
【发布时间】:2018-10-11 23:19:35
【问题描述】:

我试图了解 python 如何使用内存来估计我一次可以运行多少个进程。现在,我在具有大量内存(约 90-150GB 可用内存)的服务器上处理大文件。

为了测试,我会在python中做一些事情,然后看htop看看是什么用法。

第 1 步:我打开一个 2.55GB 的文件并将其保存为字符串

with open(file,'r') as f:
    data=f.read()

使用量为 2686M

第 2 步:我在换行符上拆分文件

data = data.split('\n')

使用量为 7476M

第 3 步:我只保留每 4 行(我删除的三行中的两行与我保留的行的长度相同)

data=[data[x] for x in range(0,len(data)) if x%4==1]

使用量为 8543M

第 4 步:我将其分成 20 个相等的块以通过多处理池运行。

l=[] 
for b in range(0,len(data),len(data)/40):
    l.append(data[b:b+(len(data)/40)])

使用量为 8621M

第 5 步:我删除数据,使用量为 8496M。

有几件事对我来说没有意义。

第二步,为什么我把字符串改成数组时内存使用量会上升这么多。我假设数组容器比字符串容器大很多?

在第三步中,为什么数据没有显着缩小。我基本上摆脱了 3/4 的数组和数组中至少 2/3 的数据。我希望它会相应地缩小。调用垃圾收集器没有任何区别。

奇怪的是,当我将较小的数组分配给另一个变量时,它使用的内存更少。 使用6605M

当我删除旧对象data使用6059M

这对我来说似乎很奇怪。任何有关缩小我的记忆足迹的帮助将不胜感激。

编辑

好吧,这让我头疼。显然 python 在幕后做了一些奇怪的事情......而且只有 python。我使用我的原始方法和下面答案中建议的方法制作了以下脚本来演示这一点。数字均以 GB 为单位。

测试代码

import os,sys
import psutil
process = psutil.Process(os.getpid())
import time

py_usage=process.memory_info().vms / 1000000000.0
in_file = "14982X16.fastq"

def totalsize(o):
    size = 0
    for x in o:
        size += sys.getsizeof(x)
    size += sys.getsizeof(o)
    return "Object size:"+str(size/1000000000.0)

def getlines4(f):
    for i, line in enumerate(f):
        if i % 4 == 1:
            yield line.rstrip()

def method1():
    start=time.time()
    with open(in_file,'rb') as f:
        data = f.read().split("\n")
    data=[data[x] for x in xrange(0,len(data)) if x%4==1]
    return data

def method2():
    start=time.time()
    with open(in_file,'rb') as f:
        data2=list(getlines4(f))
    return data2


print "method1 == method2",method1()==method2()
print "Nothing in memory"
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage
data=method1()
print "data from method1 is in memory"
print "method1", totalsize(data)
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage
del data
print "Nothing in memory"
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage
data2=method2()
print "data from method2 is in memory"
print "method2", totalsize(data2)
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage
del data2
print "Nothing is in memory"
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage


print "\nPrepare to have your mind blown even more!"
data=method1()
print "Data from method1 is in memory"
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage
data2=method2()
print "Data from method1 and method 2 are in memory"
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage
data==data2
print "Compared the two lists"
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage
del data
print "Data from method2 is in memory"
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage
del data2
print "Nothing is in memory"
print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage

输出

method1 == method2 True
Nothing in memory
Usage: 0.001798144
data from method1 is in memory
method1 Object size:1.52604683
Usage: 4.552925184
Nothing in memory
Usage: 0.001798144
data from method2 is in memory
method2 Object size:1.534815518
Usage: 1.56932096
Nothing is in memory
Usage: 0.001798144

Prepare to have your mind blown even more!
Data from method1 is in memory
Usage: 4.552925184
Data from method1 and method 2 are in memory
Usage: 4.692287488
Compared the two lists
Usage: 4.692287488
Data from method2 is in memory
Usage: 4.56169472
Nothing is in memory
Usage: 0.001798144

对于那些使用 python3 的人来说,它非常相似,除了在比较操作之后没有那么糟糕......

PYTHON3 的输出

method1 == method2 True
Nothing in memory
Usage: 0.004395008000000006
data from method1 is in memory
method1 Object size:1.718523294
Usage: 5.322555392
Nothing in memory
Usage: 0.004395008000000006
data from method2 is in memory
method2 Object size:1.727291982
Usage: 1.872596992
Nothing is in memory
Usage: 0.004395008000000006

Prepare to have your mind blown even more!
Data from method1 is in memory
Usage: 5.322555392
Data from method1 and method 2 are in memory
Usage: 5.461917696
Compared the two lists
Usage: 5.461917696
Data from method2 is in memory
Usage: 2.747633664
Nothing is in memory
Usage: 0.004395008000000006

故事的寓意...python的记忆似乎有点像Monty Python的Camelot...这是一个非常愚蠢的地方。

【问题讨论】:

  • 你的内存使用量真的在“usage is 8.543M”位中减少了3个数量级吗?
  • 显然不是,正如您在我的编辑中看到的那样;)
  • “为什么当我将字符串更改为数组时内存使用量会增加这么多”您的代码中没有数组。您创建了一个 list 对象。之前,您只有一个字符串,它是一个对象。 一切 在 Python 中都是一个对象。对于每个对象,您将拥有大约 40-50 个字节的标准对象开销(取决于系统、python 版本等)。所以现在你有一个巨大的列表,里面有很多小对象。每个指向对象的指针将额外增加 4-8 个字节
  • 好的,那么另一个大问题是你在做[data[x] for x in range(0,len(data)) if x%4==1]。请使用xrangerange 实现了整个列表,考虑到您的数据,这可能非常大。
  • @jeffpkamp 正如我之前所说,gc 只是循环引用垃圾收集器。它不会影响您在此处所做的任何事情(除非您没有向我们展示创建引用循环的代码)。无论如何,内存可能会被回收,但是当 Python 进程决定将它还给操作系统时,将取决于很多事情

标签: python memory


【解决方案1】:

我将建议您退后一步,转而以直接实现您的目标的方式来解决此问题:从一开始就减少峰值内存使用。以后再多的分析和摆弄都无法使用注定失败的方法来克服;-)

具体来说,您在通过data=f.read() 迈出第一步时走错了路。现在已经的情况是,您的程序可能无法扩展到完全适合 RAM 并有剩余空间(运行 OS 和 Python 等等)的数据文件。

您真的需要所有数据一次都在 RAM 中吗?关于后面的步骤的细节太少了,但显然不是在开始时,因为你想立即扔掉你阅读的 75% 的行。

所以从渐进式开始:

def getlines4(f):
    for i, line in enumerate(f):
        if i % 4 == 1:
            yield line

即使您只做这么多,您也可以直接跳到第 3 步的结果,从而节省大量的峰值 RAM 使用:

with open(file, 'r') as f:
    data = list(getlines4(f))

现在,RAM 峰值需求与您关心的唯一行中的字节数成正比,而不是与文件字节周期的总数成正比。

要继续取得进展,不要一次性将data 中所有感兴趣的行具体化,而是将这些行(或行块)增量地提供给您的工作进程。没有足够的细节让我为此建议具体的代码,但请牢记目标,您会弄明白的:您只需要需要足够的 RAM 来不断地向工作进程提供线路,并保存您需要保存在 RAM 中的大部分工作进程的结果。 可能,无论输入文件大小如何,峰值内存使用不需要超过“微小”。

与一开始就采用内存友好的方法相比,解决内存管理细节要困难得多。 Python 本身有几个内存管理子系统,每个都可以说很多。他们又依赖于平台 C malloc/free 设施,这方面也有很多需要学习的地方。而且我们仍然处于与您的操作系统报告的“内存使用”有任何直接关系的水平。反过来,平台 C 库依赖于特定于平台的操作系统内存管理原语,通常只有操作系统内核内存专家才能真正理解。

“为什么操作系统说我仍在使用 N GiB 的 RAM?”的答案?可以依赖这些层中任何一层中特定于应用程序的细节,甚至依赖于它们之间不幸的或多或少的意外交互。最好不要一开始就问这样的问题。

编辑 - 关于 CPython 的 obmalloc

很高兴您提供了一些可运行的代码,但没有人可以运行它,因为没有其他人拥有您的数据;-) 诸如“有多少行?”之类的问题。和“线长的分布是什么?”可能很关键,但我们无法猜测。

正如我之前提到的,应用程序特定的细节往往是超越现代内存管理器的必要条件。它们很复杂,所有级别的行为都可能很微妙。

Python 的主要对象分配器(“obmalloc”)从平台 C malloc 请求“arenas”,2**18 字节的块。只要这是您的应用程序正在使用的 Python 内存系统(无法猜测,因为我们没有您的数据可以使用),256 KiB 是内存的最小粒度从 C 级别请求或返回到 C 级别。反过来,C 级别通常有自己的“分块”策略,这些策略因 C 实现而异。

一个 Python arena 又被雕刻成 4 KiB“池”,每个池动态适应被雕刻成每个池固定大小的较小块(8 字节块、16 字节块、24 字节块、 ...,每个池 8*i 字节块)。

只要竞技场中的单个字节用于实时数据,就必须保留 整个 竞技场。如果这意味着其他 262,143 个 arena 字节未被使用,那么运气不好。正如您的输出所示,所有内存最终都返回了,那么您为什么真的在乎呢?我知道这是一个抽象有趣的谜题,但如果不努力理解 CPython 的 obmalloc.c 中的代码,你就无法解决它。作为一个开始。任何“摘要”都会遗漏对某些应用程序的微观行为实际上重要的细节。

合理:您的字符串足够短,以至于所有字符串对象标题和内容(实际字符串数据)的空间都是从 CPython 的 obmalloc 获得的。它们将散布在多个领域。竞技场可能看起来像这样,其中“H”代表分配字符串对象头的池,而“D”代表分配字符串数据空间的池:

HHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDD...

在您的method1 中,它们倾向于交替“像那样”,因为创建单个字符串对象需要分别为字符串对象标题和字符串对象数据分配空间。当你继续扔掉你创建的 3/4 的字符串时,或多或少 3/4 的空间变得可重用 Python。但是没有一个字节可以返回给系统 C,因为 仍然 现场数据散布在整个竞技场中,包含您没有丢弃的字符串对象的四分之一(这里“-”表示可用空间重复使用):

HHDD------------HHDD------------HHDD------------HHDD----...

有这么多的可用空间,事实上,不那么浪费的method2 有可能从method1 留下的-------- 孔中获得所需的所有内存,即使你不扔掉method1 结果。

为了简单起见 ;-) ,我会注意到关于如何使用 CPython 的 obmalloc 的一些细节也因 Python 版本而异。一般来说,Python 版本越新,尝试首先使用 obmalloc 而不是平台 C malloc/free 的次数越多(因为 obmalloc 通常更快)。

但是即使你直接使用平台 C malloc/free,你仍然可以看到同样的事情发生。内核内存系统调用通常比纯粹在用户空间中运行代码更昂贵,因此平台 C malloc/free 例程通常有自己的策略“向内核请求比单个请求所需更多的内存,并将其划分为更小的碎片我们自己”。

需要注意的一点:Python 的 obmalloc 和 platorm C malloc/free 实现都不会自行移动实时数据。两者都将内存地址返回给客户端,并且这些地址不能更改。 “漏洞”是两者都无法回避的生活事实。

【讨论】:

  • 我并不担心内存使用高峰...我正在运行上面的程序来减少数据,这样我就可以同时运行多个文件。即使它们产生相同的对象,您的程序也会以 1/3 的足迹生成相同的数据(参见我上面的编辑)。显然,在我的列表创建过程中,有一些东西与我的列表相关联,这会产生大量开销,并且不会随数据消失。
  • 查看大编辑了解更多信息——事情并不简单;-)
  • 我和我的老板谈过这个问题,尽管我完全没有接受过正规的 CS 培训,但我们猜测这就是我们的猜测。我很惊讶创建一个新对象并将部分分配给该对象并没有获得新的内存块,但我想保持引用相同的内存比获取一个新的部分并写入它更有效。同样令人惊讶的是,它不会检查空位并定期合并。经验教训:动态生成对象,因为削减并不总是释放内存。
  • 与其说是“计算机科学”,不如说是在速度与空间与复杂性与...之间的权衡... CPython 做出了对 it 最有意义的选择,因为它是有意为之的与 C 代码一起工作。 Jython 是在 Java 虚拟机上运行的实现,其行为有所不同:Java 实现了压缩(“消除漏洞”)垃圾收集,而 Jython 继承了它。这并不简单。示例:CPython 中的id(x) 是免费的,因为它返回 x 的内存地址,该地址永远不会改变。但这对 Jython 来说很难,因为 Java 可能随时移动内存中的对象。
  • @TimPeters 有没有办法可视化竞技场的实时状态?
猜你喜欢
  • 2011-03-09
  • 2012-09-04
  • 2019-04-18
  • 1970-01-01
  • 1970-01-01
  • 2020-12-10
  • 2015-01-13
  • 2016-11-27
相关资源
最近更新 更多