未预料到的Python字典行为
我有一段代码:未预料到的Python字典行为,python,performance,dictionary,Python,Performance,Dictionary,我有一段代码: 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
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秒才能运行
但是,如果我注释掉deld[I]
或内部循环,它会在0.1秒内运行。这是因为
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次运行中是一致的…)
您遇到的问题是由于对一个字典的一个元素(例如,
next(iter(d))
)进行迭代而引起的,该字典曾经很大,但已经缩小了很多。如果您的哈希值不走运,这几乎会很慢,因为迭代所有字典项。这段代码非常“不走运”(可以预见,这是由于Python哈希设计)
出现此问题的原因是,删除项时Python不会重建字典的哈希表。因此,一个字典的哈希表(过去有200000个条目,但现在只剩下1个)仍然有200000多个空格(可能更多,因为它在峰值时可能还没有完全填满)
当您在字典中包含所有值的情况下迭代字典时,查找第一个值非常简单。第一个将位于前几个表条目中的一个。但是,当您清空表时,表的开头将出现越来越多的空格,搜索仍然存在的第一个值将花费越来越长的时间
如果您使用的是整数键(大部分)散列到自身(只有
-1
散列到其他内容),则情况可能更糟。这意味着“完整”词典中的第一个键通常是0
,下一个1
,依此类推。当您以递增的顺序删除值时,您将首先非常精确地删除表中最早的键,从而使搜索最大程度地变得更糟。删除项目后,访问整个键似乎会有一些性能成本。如果您直接访问,则不会产生此成本。因此,我猜想,当删除项时,字典会将其键列表标记为脏,并在更新/重建之前等待对键列表的引用
这解释了为什么在删除内部循环时(没有导致重新生成键列表),性能没有受到影响。这也解释了为什么删除
del d[i]
行时循环速度很快(您没有标记要重建的键列表)。奇怪的是,如果我删除内部for循环,则for key in d:
只需执行del d[i]
,只需几秒钟。…@juanpa.arrivillaga,是的,为什么会这样?这并不能真正解释为什么删除内部循环,但保持deld[i]
它返回得很快。我无法解释这一点,更奇怪的是,如果我删除d:循环中的键的,它会回到需要几分之一秒的时间!嗯,Raymond Hettinger和其他核心开发人员有时会回答这个标签上的问题,所以希望他们会出现……啊,这是在迭代器上第一次调用next
,这需要时间;用类似于di=iter(d)的东西替换该回路;next(di)
运行时间回到15秒。我可以使用2.7进行复制,因此这看起来与版本无关。ma_值路径似乎是用于密钥共享字典,通常用于对象属性dict(将其密钥放入所有实例共享的对象中),而不是用于普通dict。