Python 3.x 将命名元组附加到字典中的列表时会导致大量内存膨胀

Python 3.x 将命名元组附加到字典中的列表时会导致大量内存膨胀,python-3.x,Python 3.x,快速入门指南:在python 3.5中,我希望占用大约5GB的内存,而不是占用超过15GB的内存,然后由于缺乏资源而崩溃 import pickle from collections import namedtuple Hdr1 = namedtuple("Hdr1", "id hash source options elements locations") Hdr2 = namedtuple("Hdr2", "id hash source options stream locations")

快速入门指南:在python 3.5中,我希望占用大约5GB的内存,而不是占用超过15GB的内存,然后由于缺乏资源而崩溃

import pickle
from collections import namedtuple

Hdr1 = namedtuple("Hdr1", "id hash source options elements locations")
Hdr2 = namedtuple("Hdr2", "id hash source options stream locations")
Hdr3 = namedtuple("Hdr3", "id hash source options series locations")
Identifier = namedtuple("Identifier", "id hash")
Location = namedtuple("Location", "index tell")
IndexData = namedtuple("IndexData", "filenames packet1 packet2 packet3")

filenames = [] # filled by other code, but it's a list, with 10 items
packet1_d = {}
packet2_d = {}
packet3_d = {}

index_data = IndexData(filenames, packet1_d, packet2_d, packet3_d)

# for each file
# read all the packets in the file, get the tell() location each time
if packet is header:
  if packet is packet1_header:
    packet1_d[Identifier(id, hash)] = Hdr1(id, hash, source, options, [])
  elif packet is packet2_header:
    packet2_d[Identifier(id, hash)] = Hdr2(id, hash, source, options, stream, [])
  else 
    packet3_d[Identifier(id, hash)] = Hdr3(id, hash, source, options, series, [])
else
  loc = Location(index, tell)
  # This part below is deadly
  if packet is packet1:
    packet1_d[Identifier(id, hash)].locations.append(loc)
  if packet is packet2:
    packet2_d[Identifier(id, hash)].locations.append(loc)
  if packet is packet3:
    packet3_d[Identifier(id, hash)].locations.append(loc)

pickle.dump(index_data, open("index_data.p", "wb"))
详细信息:这显然不是全部代码-我保留了打开和解析文件的部分,显然您没有可用的文件,因此无法重现问题。
is
语句是伪代码,但在逻辑上是等价的。这是我如何设置数据结构的真实表现,因此对内存使用情况的估计将是准确的,并且它准确地描述了变量的使用方式,因此对于查找内存泄漏应该具有代表性

当我注释掉我注释为“致命”的6行时,在运行了10GB的数据和大约100M的数据包之后,pickle文件(仅包含文件名和不同数据包头的列表)大约在5到10MB之间。我知道pickle压缩,但这仍然意味着“基本数据”小于50MB

总共有91116480个数据包。为了便于计算,我们就称之为100米吧。每个
位置
只是文件列表的索引和
file.tell()返回的文件。交互式shell中的经验测试表明,每个“位置”为64字节:

>>> import sys
>>> from collections import namedtuple
>>> Location = namedtuple("Location", "idx tell")
>>> fobj = open("/really/big/data.file", "rb")
>>> fobj.seek(1000000000)
1000000000
>>> tell = fobj.tell()
>>> loc = Location(9, tell)
>>> sys.getsizeof(loc)
64
因此,总内存使用量应不超过6.4 GB

为什么这会占用超过15GB的内存?是否有一种更有效的内存设置方法


我通过将所有数据放入sqlite数据库文件来解决这个问题。整个文件为2.1GB,因此原始数据不应超过2.1GB。我可以理解Python中的开销,这将使它达到6GB的范围,但它不应该达到15GB。尽管我已经解决了这个问题,但我想知道下次如何避免它。

我想你主要是在问一个实现细节——很难说Python对象何时会使用2Kb而不是500b——即使你解决了你正在跟踪的精确问题,直到你的数据大小再次翻倍,这也会很好

您需要的是切换到流式方法—在这种方法中,您可以根据需要读取/处理/写入数据。这将意味着改变输出格式——它甚至可以是一个“pickle文件”,但您可以对较小的对象进行pickle,而不是单一的字典(甚至可能是一系列小字典,它们在读取时只是作为“更新”应用在彼此的顶部)


但是,如果您将输出切换为sqlite数据库(在该数据库中,您甚至可以将所需的对象作为列数据进行Pickle处理),那么对于这些数据以及更多数据,您最好将它们转换为一个对象列表。该数组在内存中表示为一个高效的C样式数组,因此N个32位数字的列表只需要N*4字节的内存

您的
位置
类型只有一个
索引
和一个
告诉
,因此如果它们都是32位整数,您可以像这样使用
'i'
类型代码(为简洁起见,仅显示了
packet1
大小写):


(编辑为使用namedtuple LocationArray而不是普通的tuple。)

正如我在另一个答案中所说,在这种情况下,最好将数据保存在磁盘中,由数据库系统管理

您面临的问题是,尽管名称耦合中的每个字段(包括仅包含数值的字段)都很紧凑,但它们都是完整的Python对象。Python中的整数确实使用了约30个字节,即每个字段加上namedtuple对象大小本身约64个字节

在标准库中,
ctypes
模块具有一个“结构”基类型,可以创建对象记录数组,其中每个记录只使用其数据所需的字节数。也就是说,如果使用1个4字节整数和1个8字节整数创建结构,则每条记录将占用12个字节数百个字节,用于获取有关阵列本身的信息。
ctypes.Structure
数组的问题是,您必须先创建一个固定大小的数组,然后再创建一个固定大小的数组-不可能简单地将更多的记录追加到它们的末尾。如果为每条记录创建一个独立的结构对象,那么每条记录的开销也会达到100字节

Numpy是Python处理大数字的事实库,Pandas的底层引擎(在更高的层次上,它可能是解决问题的更高解决方案)允许您使用指定的记录创建数组,确定每条记录的字节类型。但是普通的numpy数组也有相同的问题,即大小是固定的——不能只向数组中添加任意记录

熊猫--可能是你应该在那里使用的

但如果您没有,我已经组合了两个类,它们利用Python的stdlib“struct”在内存中只存储数据,允许每个12字节的记录只使用12个字节,其他什么都不用,而且它是可pickle的

您可以按原样使用文件-每个“StrutureSequence”对象的创建或多或少类似于一个namedtuple,再加上此处记录的记录结构信息
在您的代码中,只需使用StructSequence的一个实例来创建列表,您甚至可以在这些序列中附加一个(与字段兼容的)namedtuple对象,它们只会将数据存储在内存中。泡菜也可以和他们一起吃。

谢谢。我曾考虑过将整块巨石分解成更小的部分,但随后我不得不将它们全部加载到内存中,以便进行一些搜索。不过,数据库的想法对我来说效果很好。我必须研究炼金术;现在,sqlite似乎正在做我需要的一切。事实上,这是一个tpo-我打算写的只是sqlite Sqlalchemy可能有点过头了(它不是一个数据库,它是一个提供多个后端的ORM)是的,听起来不错
import array
LocationArray = namedtuple("LocationArray", "index tell")
if packet is header:
    locations = LocationArray(index=array.array('i'), tell=array.array('i'))
    packet1_d[Identifier(id, hash)] = Hdr1(id, hash, source, options, locations)
else:
    loc = packet1_d[Identifier(id, hash)].locations
    loc.index.append(index)
    loc.tell.append(tell)