【问题标题】:Compile to byte code takes up too much memory编译成字节码占用太多内存
【发布时间】:2011-10-16 08:12:40
【问题描述】:

我需要将一个非常大的字典导入 python,我遇到了一些意想不到的内存瓶颈。字典有形式,

d = {(1,2,3):(1,2,3,4), (2,5,6)=(4,2,3,4,5,6), ... }

所以每个键都是一个 3 元组,每个值都是一个相对较小的任意大小的元组(可能永远不会超过 30 个元素)。使字典变大的是键的数量。我正在使用的一个较小的示例大约有 247257 个键。我通过模拟生成这个字典,所以我可以写出一个定义这个字典的文本文件,对于我刚才提到的例子,这是一个 94MB 的文件。我遇到的瓶颈是初始编译为 python 字节码会消耗大约 14GB 的内存。因此,当我第一次导入字典时,我看到 RAM 使用率飙升,并且在 10 秒后所有内容都已加载。如果 .pyc 文件已经生成,则导入几乎是即时的。使用 pymler,我确定这本字典的内存只有大约 200 MB。这里有什么交易?关于如何将此字典加载到 python 中或至少编译为字节码,我是否还有其他选择。我正在用 C++ 运行生成模拟,但我无法以我需要的任何格式编写文件。那里有任何选项(python库等)吗?我正在与一些需要将此数据作为字典的软件进行交互,因此请不要在该领域提出其他建议。另外,如果您想知道,我已经在文本文件中定义了字典,就像上面的定义一样,

d = {}
d[1,2,3] = (1,2,3,4)
d[2,5,6] = (4,2,3,4,5,6)
...

两者在编译为字节码时都会产生相同的内存峰值。事实上,第二个似乎稍微差一点,这让我感到惊讶。必须有某种方法来控制初始编译所需的 ram 数量。似乎它应该能够以某种方式一次编译一个键值对。有什么想法吗?

其他信息: 使用 python 2.6.5

【问题讨论】:

  • 您是否将数据分隔在文件的多行中?
  • 两种方法我都试过了:1)单个巨型字典定义,2)多行 d[key] = value
  • 为什么不使用pprint.pformat 将字典写入文件并在加载时使用evalast.literal_eval
  • 这在我上周工作的 60MB 矩阵上运行良好...
  • 我可能是错的,但听起来这些是 python 函数。使用它们将涉及首先将字典加载到 python 中,这正是问题所在。还是这些 C++ 建议?

标签: python memory dictionary bytecode


【解决方案1】:

我想问题在于,在解析文件时,会生成一个巨大的语法树,而每个元素加起来的开销很小。生成字节码后,不再需要语法树并将其转储,从而产生 200MB 的数据。

您是否尝试过将数据按以下格式存储在单独的文件中,然后在 python 中动态加载?

1,2,3=1,2,3
2,5,6=4,2,3,4,5,6

Python 脚本应如下所示:

file = open("filename")
d = {}

for line in file:
    key, val = line.split("=")
    key = tuple(key.split(","))
    d[key] = tuple(val.split(","))

file.close()

【讨论】:

    【解决方案2】:
    【解决方案3】:

    我猜当您执行“导入模块_包含_humungous_dict_statement”时,您的编译高峰会发生。那么无论你只有一个语句还是 247257 个单独的赋值语句都没关系,整个模块仍然会立即编译。您可以尝试使用单独的分配语句表单,然后打开文件,一次读取一行,然后执行它。那么你一次只能编译一行。可能需要一段时间。

    【讨论】:

    • 这个方法能够克服内存峰值,它实际上与 python 编译的速度相当(如果不是稍微快一点的话)。这可能是最好的选择。但是,当尝试腌制字典时,内存峰值再次发生。我只是希望有一种方法可以以某种低级形式保存字典,以便 python 可以快速再次读取。想法?
    • 对不起,我收回最后的评论。这个 DID 还克服了腌制字典时的内存峰值。所以回顾一下,导入字典文件和酸洗都会产生单独的内存峰值(14+ GB)。但是请注意,只要从导入中创建了 .pyc 文件,就不需要酸洗,因为现在可以快速导入,而不会产生大量内存开销。我这样做只是为了仔细检查。但是,如果我遍历文件中的每一行并执行(行),然后泡菜,我永远不会看到超过 600MB 的内存使用量(与 14GB 峰值相比)。
    • 那么,当然,一旦字典被腌制,它就可以很容易地重新加载,而不会出现内存峰值。我很想听听任何人解释为什么会发生这种情况。
    • 解酸不需要任何实际的编译,只需将酸洗的数据解析成实际的对象。所以在那个级别上,可能非常类似于一次执行一行的方法。
    • 我同意,我只是对最初的酸洗而不是解酸感到奇怪。
    【解决方案4】:

    我怀疑创建用作键的列表很昂贵。定义一个函数,将三元组的三个部分作为输入并返回一个管道分隔的字符串。用它作为你的钥匙。

    【讨论】:

    • 这个想法似乎没有解决问题。在编译期间仍然有相同的内存峰值。
    【解决方案5】:

    按照我阅读您的问题的方式,您正在模拟器中生成 Python 源代码,并且生成的源代码具有硬编码的巨型字典的内容。如果这是真的,那么你可以很容易地生成这个:

    def giantdict():
      d0 = {(1, 2): (3, 4), (3, 4): (5, 6), ...}  # first 1000 key/value pairs here
      d1 = {(1, 2): (3, 4), (3, 4): (5, 6), ...}  # next 1000 key/value pairs
      d2 = {(1, 2): (3, 4), (3, 4): (5, 6), ...}  # next 1000 key/value pairs
      d3 = {(1, 2): (3, 4), (3, 4): (5, 6), ...}  # next 1000 key/value pairs
      # ... until you're done
      bigd = d0
      bigd.update(d1)
      del d1
      bigd.update(d2)
      del d2
      # ... continue updating with all the dN dictionaries
      return bigd
    

    我不确定这是否会缩短编译时间,但可以尝试一下。如果在编译时将所有内容放在一个数据结构中会受到惩罚,那么在运行时将其拆分并组装可能会解决这个问题。

    虽然这种代码(我的或你的)如果是人类编写的会引起我的愤怒和愤怒,但我认为生成的代码没有必要“很好”,只要你知道没有人需要阅读或维护它。

    【讨论】:

    • 这个方法仍然有同样的内存问题,即使最后没有创建单个大字典。
    【解决方案6】:

    这是一个使用 defaultdict 自动嵌套索引值的类,并带有一些特殊的 __getitem____setitem__ 方法来接受元组作为参数:

    from collections import defaultdict
    
    defdict3level = (lambda : defaultdict(lambda : 
                                defaultdict( lambda : 
                                    defaultdict(tuple))))
    
    class dict3level(object):
        def __init__(self):
            self.defdict = defdict3level()
    
        def __getitem__(self, key):
            if isinstance(key, tuple):
                if len(key)==3:
                    return self.defdict[key[0]][key[1]][key[2]]
                elif len(key)==2:
                    return self.defdict[key[0]][key[1]]
                elif len(key)==1:
                    return self.defdict[key[0]]
            else:
                return self.defdict[key]
    
        def __setitem__(self, key, value):
            if isinstance(key, tuple) and len(key)==3:
                self.defdict[key[0]][key[1]][key[2]] = value
            else:
                self.defdict[key] = value
    
        def __getattr__(self, attr):
            return getattr(self.defdict, attr)
    

    现在像以前一样执行所有任务:

    d = dict3level()
    d[1,2,3] = (1,2,3,4)
    d[1,2,7] = (3,4,5,6)
    d[2,5,6] = (4,2,3,4,5,6)
    

    您仍然可以获得特定元组的特定条目:

    # get a specific entry
    print d[1,2,3]
    

    但您也可以按级别导航您的字典:

    # get all different 0'th index values
    print d.keys()
    
    # get all sub values in d[1,2,*]
    print d[1,2].keys()
    for key in d[1,2]:
        print "d[1,2,%d] = %s" % (key, d[1,2][key])
    
    # no such entry, return empty tuple
    print d[1,2,0]
    

    给予:

    print d[1,2,3] -> (1, 2, 3, 4)
    print d.keys() -> [1, 2]
    print d[1,2].keys() -> [3, 7]
    for key in d[1,2]:... -> 
        d[1,2,3] = (1, 2, 3, 4)
        d[1,2,7] = (3, 4, 5, 6)
    print d[1,2,0] -> ()
    

    (不知道这将如何影响您的记忆和/或酸洗问题,但生成的结构具有更多功能。)

    【讨论】:

    • 我会试试这个,让你知道内存。
    【解决方案7】:
    猜你喜欢
    • 2015-10-14
    • 2013-07-18
    • 2013-07-11
    • 2020-05-18
    • 1970-01-01
    • 1970-01-01
    • 2013-08-26
    • 1970-01-01
    • 2011-09-20
    相关资源
    最近更新 更多