【问题标题】:pickle: slow dict deserializationpickle:缓慢的字典反序列化
【发布时间】:2020-05-06 15:42:39
【问题描述】:

假设我有一个“相当大”的字典,其中键是具有重 __eq__ 函数的对象

class MyObject():

     def __eq__(self, other):
         return <very heavy function call>

     def __hash__(self):
         return <not so heavy hash calculation>

mydict = {<MyObject>:<Int>}

问题是当我尝试解开腌制的物体时,它需要很多时间。我相信这是因为pickle没有保存内部哈希表,并且在恢复它时会重新计算字典。

我做了一个简单的实验:

import pickle 
import pickletools 
original = { 'a': 0, 'b': [1, 2, 3] } 
pickled = pickle.dumps(original) 
pickletools.dis(pickled)

结果:

    0: \x80 PROTO      3
    2: }    EMPTY_DICT
    3: q    BINPUT     0
    5: (    MARK
    6: X        BINUNICODE 'a'
   12: q        BINPUT     1
   14: K        BININT1    0
   16: X        BINUNICODE 'b'
   22: q        BINPUT     2
   24: ]        EMPTY_LIST
   25: q        BINPUT     3
   27: (        MARK
   28: K            BININT1    1
   30: K            BININT1    2
   32: K            BININT1    3
   34: e            APPENDS    (MARK at 27)
   35: u        SETITEMS   (MARK at 5)
   36: .    STOP
highest protocol among opcodes = 2

没有哈希树的迹象。这意味着Pickle Machine 必须在反序列化后重新计算哈希树。但它真的是假的吗?为什么 pickle 不能保存字典的内部状态以及如何与之抗争?

【问题讨论】:

  • pickle 格式在 Python 实现中是标准化的——它们不能(仅)存储足以在一个解释器的一个版本中实现dict 的信息。跨度>
  • 需要另存为泡菜吗?如果您可以将其保存为 json 似乎更有效。
  • @Gust,它对我不起作用,因为加载后我需要一个 python dict。如果我将使用json 构建python dict afterload 将需要相同的时间。如果是这样,我需要一种方法来使用预先计算的哈希值初始化 python dict
  • 如果它实际上在加载过程中多次调用__eq__,它也会在使用过程中多次调用__eq__,并且程序的其余部分也会很慢。在 unpickling 期间对__eq__ 的大量调用表明__hash__ 有问题。

标签: python serialization pickle


【解决方案1】:
  • __eq__ 方法仅在存在哈希冲突时在插入dict 时调用(不同对象的__hash__ 方法返回相同的答案);如果这种情况经常发生,足以导致明显的减速,那么它也会大大降低所有其他操作的速度——在极端情况下,如果 __hash__ 方法总是返回相同的答案,而不管对象的值如何,@ 987654325@ 方法将被调用 ½n² 次,仅用于 unpickle。

    如果发生这种情况,您需要改进您的 __hash__ 方法,以便它返回更好的答案(对象的不同值的重复次数更少)。您可以通过在对象的代表性样本上手动收集 __hash__ 方法的结果并确保它们大部分不同来进行检查。

  • 如果上述方法不能解决问题,我建议分析代码以确认瓶颈所在;那么你可以看看你是否可以加速成为瓶颈的功能。

【讨论】:

    【解决方案2】:

    Pickle Machine 是跨各种 Python 对象实现的,某些对象可能不喜欢哈希实现,并且可能会刹车。

    如果您要做的只是保存 Json 对象,请使用 json 对象,因为它会很有效。

    【讨论】:

    • 它对我不起作用,因为加载后我需要一个 python 字典。如果我将使用 json 构建 python dict 将有相同的时间。
    【解决方案3】:

    使用腌制对象作为字典键。

    通过使用 pickled 对象作为 key,我们可以完全绕过 MyObject.__hash__MyObject.__eq__ 方法,并有可能加快 unpickling 过程。

    一旦你有了 unpickled 的字典,你可以慢慢地根据需要将 pickeled 的键更改为实际的 MyObject 实例。

    p1.py

    创建一个字典,其中键的类型为 MyObject,然后将它们腌制到一个文件中。 创建一个字典,其中键是腌制的 MyObject 字节。 比较解开每个字典所需的时间。

    #!/usr/bin/env python3
    
    import pickle
    import pickletools
    from pprint import pprint
    import random
    import string
    import time
    
    random.seed(19891225)
    
    
    class MyObject(object):
    
        def __init__(self, value):
            self.value = value
    
        def __eq__(self, other):
            exit(1)
            return self.value == other.value
    
        def __hash__(self):
            time.sleep(10)
            return hash(tuple(self.value))
    
    
    def key_value():
        value_len = random.randint(0,len(string.ascii_letters))
        value = random.sample(string.ascii_letters, value_len)
        return (MyObject(value), value_len)
    
    def object_key_dict(size=10):
        return dict([key_value() for i in range(0, size)])
    
    def pickle_key_dict(d):
        return {pickle.dumps(k):v for k, v in d.items()}
    
    def pickle_unpickle(obj, filename):
        with open(filename, "wb") as pout:
            pickle.dump(obj, pout)
    
        print(f"Unpickle Start: {time.strftime('%H:%M:%s')}")
        with open(filename, "rb") as pin:
            newobj = pickle.load(pin)
        print(f"Unpickle complete: {time.strftime('%H:%M:%s')}")
        print("-"*20)
    
        return newobj
    
    
    if __name__ == "__main__":
        d = object_key_dict(size=10)
    
        print("Use MyObject instances as dictionary keys")
        unpickled_d = pickle_unpickle(d, 'doc.p')
    
        print("\nUse pickled ojbects as dicitonary keys")
        pd = pickle_key_dict(unpickled_d)
        upd = pickle_unpickle(pd, 'docp.p')
    

    输出

    Use MyObject instances as dictionary keys
    Unpickle Start: 09:25:1589808319
    Unpickle complete: 09:26:1589808419
    --------------------
    
    Use pickled ojbects as dicitonary keys
    Unpickle Start: 09:26:1589808419
    Unpickle complete: 09:26:1589808419
    --------------------
    

    【讨论】:

    • 发生哈希冲突时会调用eq方法;您可以通过从 hash 返回一个常量来测试它。
    • 可能是这样;但是如果使用腌制对象作为键,它仍然会绕过对象的hash和eq方法。此外,我尝试了使用重复对象的代码,并且在 unpickling 过程中仍然没有调用 eq 方法。这是这个问题的症结所在。
    • 我只是仔细检查了,__eq__ 实际上是在 unpickling 期间调用的;这适用于不同的对象(不相等,__eq__ 返回False),但具有相同的哈希值,使用def __hash__(self): return 1 来确保这一点。 (如果对象相等,您的 object_key_dict 函数将返回一个只有一个元素的字典。)
    • 我并不是说永远不会调用 eq。但是你不是通过构建一个总是返回 1 的散列函数来强迫一个更坏的情况吗?你我绝对同意。是哈希函数导致了这个问题的问题。如果没有更多与业务规则、实施和数据量相关的信息,我们将无能为力。
    • 是的,我强制采用最坏的情况来验证 __eq__ 实际上可以在 unpickling 期间调用。但是,如果 __eq__ 方法在 unpickling 中显着减慢,我怀疑实际情况与此相去不远......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多