【问题标题】:Unanticipated Python Dictionary Behavior意料之外的 Python 字典行为
【发布时间】:2020-02-25 20:40:21
【问题描述】:

我有这段代码:

import time

d = dict()
for i in range(200000):
    d[i] = "DUMMY"

start_time = time.time()

for i in range(200000):
    for key in d:
        if len(d) > 1 or -1 not in d:
            break
    del d[i]

print("--- {} seconds ---".format(time.time() - start_time))

为什么这需要大约 15 秒才能运行?

但是,如果我注释掉 del d[i] 或内部循环,它会在 ~0.1 秒内运行。

【问题讨论】:

  • 更奇怪的是,如果我删除 for key in d: 循环,它会回到几分之一秒!
  • 嗯,Raymond Hettinger 和其他核心开发人员有时会回答有关此标签的问题,所以希望他们能来...
  • 啊,这是第一次在迭代器上调用next 需要时间;用 di = iter(d); next(di) 之类的东西替换循环,运行时间回到 15 秒。
  • 我可以用 2.7 重现,所以这看起来与版本无关..
  • 看起来ma_values 路径用于键共享字典,通常用于对象属性字典(将它们的键放入所有实例共享的对象中),而不是普通字典。

标签: python performance dictionary


【解决方案1】:

您遇到的问题是由于迭代曾经很大但已大幅缩小的字典的一个元素(例如next(iter(d)))引起的。如果您对哈希值不走运,这可能会因为迭代所有字典项而变得几乎很慢。而且这段代码非常“不走运”(可以预见的是,由于 Python 哈希设计)。

问题的原因是当您删除项目时 Python 不会重建字典的哈希表。因此,曾经有 200000 个项目但现在只剩下 1 个项目的字典的哈希表仍然有超过 200000 个空格(可能更多,因为它可能在峰值时没有完全填满)。

当您在字典中包含所有值时迭代字典时,找到第一个非常简单。第一个将在前几个表条目之一中。但是当你清空表格时,表格开头的空格会越来越多,搜索仍然存在的第一个值会花费越来越长的时间。

考虑到您使用的是整数键,这可能会更糟,它(主要)散列到自己(只有-1 散列到其他东西)。这意味着“完整”字典中的第一个键通常是0,下一个是1,依此类推。当您以递增的顺序删除值时,您将非常精确地首先删除表中最早的键,从而最大限度地降低搜索质量。

【讨论】:

    【解决方案2】:

    因为这个

    for key in d:
        if len(d) > 1 or -1 not in d:
            break
    

    将在第一次迭代时中断,因此您的内部循环基本上是无操作的。

    添加del[i] 让它做一些真正的工作,这需要时间。

    更新:以上显然是一种简单化的方式:-)

    以下版本的代码显示了相同的特征:

    import time
    import gc
    n = 140000
    
    def main(d):
        for i in range(n):
            del d[i]        # A
            for key in d:   # B
                break       # B
    
    import dis
    d = dict()
    for i in range(n):
        d[i] = "DUMMY"
    
    
    print dis.dis(main)
    start_time = time.time()
    main(d)
    print("--- {} seconds ---".format(time.time() - start_time))
    

    使用 iterkeys 并没有什么不同。

    如果我们在不同大小的 n 上绘制运行时间,我们会得到(x 轴上的 n,y 轴上的秒数):

    很明显,正在发生指数级的事情。

    删除线 (A) 或线 (B) 会删除指数分量,尽管我不确定为什么。

    更新 2:根据@Blckknght 的回答,我们可以通过不频繁地重新散列项目来恢复一些速度:

    def main(d):
        for i in range(n):
            del d[i]
            if i % 5000 == 0:
                d = {k:v for k, v in d.items()}
            for key in d:
                break
    

    或者这个:

    def main(d):
        for i in range(n):
            del d[i]
            if i % 6000 == 0:
                d = {k:v for k, v in d.items()}
            try:
                iter(d).next()
            except StopIteration:
                pass
    

    在大 n 上花费的时间不到原始时间的一半(130000 处的凹凸在 4 次运行中是一致的......)。

    【讨论】:

    • 很奇怪,如果我删除内部 for 循环 for key in d: 并简单地执行 del d[i],它只需要几分之一秒...
    • @juanpa.arrivillaga,是的,这是为什么呢?
    • 这并不能真正解释为什么删除 inner 循环但保持del d[i] 它会很快返回。我无法解释这个
    【解决方案3】:

    删除项目后访问整个密钥似乎会产生一些性能成本。当您进行直接访问时不会产生此成本,所以我的猜测是字典在删除项目时将其键列表标记为脏,并在更新/重建之前等待对键列表的引用。

    这解释了为什么在移除内部循环时不会影响性能(您不会导致重新构建密钥列表)。它还解释了为什么当您删除 del d[i] 行时循环会很快(您没有标记要重建的密钥列表)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-05
      相关资源
      最近更新 更多