了解python中的内存使用情况

jef*_*amp 5 python memory

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

对于测试,我会在python中做一些事情,然后查看htop以查看用法是什么.

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

with open(file,'r') as f:
    data=f.read()
Run Code Online (Sandbox Code Playgroud)

用法是2686M

第2步:我将文件拆分为换行符

data = data.split('\n')
Run Code Online (Sandbox Code Playgroud)

用法是7476M

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

data=[data[x] for x in range(0,len(data)) if x%4==1]
Run Code Online (Sandbox Code Playgroud)

用量是8543M

第4步:我将其拆分为20个相等的块以运行多处理池.

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

用量是8621M

第五步:我删除数据,用法是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
Run Code Online (Sandbox Code Playgroud)

OUTPUT

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
Run Code Online (Sandbox Code Playgroud)

对于那些使用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
Run Code Online (Sandbox Code Playgroud)

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

Tim*_*ers 5

我建议您退出并以直接解决目标的方式处理此问题:缩短峰值内存使用量.使用注定的方法开始时,没有多少分析和摆弄可以克服;-)

具体地说,你在第一步就走错了路data=f.read().现在已经是这样的情况,你的程序不可能扩展到完全适合RAM的数据文件,还有空间(运行操作系统和Python和...).

您是否真的需要一次将所有数据都放在RAM中?关于后续步骤的细节太少,但显然不是在开始时,因为你立刻想要丢掉你读过的75%的行.

因此,首先要逐步执行此操作:

def getlines4(f):
    for i, line in enumerate(f):
        if i % 4 == 1:
            yield line
Run Code Online (Sandbox Code Playgroud)

即使你没有做任何事情,你可以直接跳到第3步的结果,节省大量的峰值RAM使用:

with open(file, 'r') as f:
    data = list(getlines4(f))
Run Code Online (Sandbox Code Playgroud)

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

要继续取得进步,不要data在一个巨大的吞噬中实现所有感兴趣的行,而是将行(或行的行)递增地提供给您的工作进程.我没有足够的细节为此建议具体的代码,但要记住目标并且你会弄明白:你只需要足够的RAM来保持逐步向工作流程提供线路,并且可以节省大量的工作者处理你需要保留在RAM中的结果.这是可能的峰值内存使用并不需要比"微小的"多,无论输入文件的大小.

相反,打击内存管理细节要比采用内存友好的方法更难.Python本身有几个内存管理子系统,关于每个子系统都可以说很多.他们反过来依赖于平台C malloc /免费设施,其中还有很多东西需要学习.而且我们仍然没有达到与操作系统报告的"内存使用"直接相关的水平.平台C库反过来依赖于特定于平台的OS内存管理原语,这些原语通常只有OS内核内存专家才能真正理解.

回答"为什么操作系统说我还在使用N GiB的RAM?" 可以依赖于这些层中的任何一个层中的特定于应用程序的细节,甚至可以依赖于它们之间偶然的或多或少的偶然交互.最好安排不要开始提出这样的问题.

编辑 - 关于CPython的obmalloc

你给了一些可运行的代码真是太棒了,但不是那么好,除了你之外没有人可以运行它,因为没有其他人拥有你的数据;-)诸如"有多少行?"之类的东西.和"线长的分布是什么?" 可能很关键,但我们无法猜测.

正如我之前提到的,通常需要特定于应用程序的细节来超越现代内存管理器.它们很复杂,所有级别的行为都很微妙.

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

Python竞技场依次被划分为4个KiB"池",每个池都动态地适应每个池固定大小的小块(8字节块,16字节块,24字节块,...... ,每个池8*个字节块.

只要竞技场中的单个字节用于实时数据,就必须保留整个竞技场.如果这意味着其他262,143个竞技场字节未使用,那就太难了.如您的输出所示,最后返回所有内存,那么为什么你真的关心?我知道这是一个抽象有趣的谜题,但你不会解决它,而不是花费大量精力来理解CPython中的代码obmalloc.c.作为一个开始.任何"摘要"都会遗漏一些对某些应用程序的微观行为非常重要的细节.

合理:你的字符串足够短,所有字符串对象标题和内容(实际字符串数据)的空间都是从CPython的obmalloc获得的.它们将在多个竞技场上散落.竞技场可能看起来像这样,其中"H"表示分配了字符串对象标题的池,"D"池用于分配字符串数据的空间:

HHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDD...
Run Code Online (Sandbox Code Playgroud)

method1因为创建单个字符串对象需要为字符串对象标题和字符串对象数据分别分配空间,因此在它们中它们会倾向于"像那样".当你继续抛出你创建的字符串的3/4时,那个空间的3/4可以重复使用到Python.但是没有一个字节可以返回到系统C,因为在整个竞技场中仍然有实时数据,包含你没有扔掉的四分之一的字符串对象(这里" - "表示可以重用的空间):

HHDD------------HHDD------------HHDD------------HHDD----...
Run Code Online (Sandbox Code Playgroud)

事实上,有太多的自由空间,即使你不丢弃结果,浪费较少的人method2也可以从--------留下的洞中获得所需的所有记忆.method1method1

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

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

值得注意的是:Python的obmalloc和platorm C malloc/free实现都没有自己移动实时数据.两者都将内存地址返回给客户端,而这些地址无法更改."洞"是两者之间不可避免的生活现实.