首页 > 解决方案 > 了解python中的内存使用情况

问题描述

我试图了解 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……这是一个非常愚蠢的地方。

标签: pythonmemory

解决方案


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

具体来说,你在第一步就走错了路,通过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 库又依赖于特定于平台的 OS 内存管理原语,通常只有 OS 内核内存专家才能真正理解。

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

编辑 - 关于 CPython 的 obmalloc

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

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

Python 的主要对象分配器(“obmalloc”)从平台 C malloc 请求“arenas”,2**18 字节的块。只要这是您的应用程序正在使用的 Python 内存系统(无法猜测,因为我们没有您的数据可以使用),256 KiB 是请求或返回内存的最小粒度, 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中获得所需的所有内存。--------method1method1

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

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

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


推荐阅读