python - 在Python中保留大型字典会影响应用程序性能

标签 python dictionary garbage-collection

我在理解(并最终解决)为什么记忆中有一本大字典会使其他字典的创建时间更长。
这是我使用的测试代码

import time
def create_dict():
    # return {x:[x]*125 for x in xrange(0, 100000)}
    return  {x:(x)*125 for x in xrange(0, 100000)}  # UPDATED: to use tuples instead of list of values


class Foo(object):
    @staticmethod
    def dict_init():
        start = time.clock()
        Foo.sample_dict = create_dict()
        print "dict_init in Foo took {0} sec".format(time.clock() - start)

if __name__ == '__main__':
    Foo.dict_init()
    for x in xrange(0, 10):
        start = time.clock()
        create_dict()
        print "Run {0} took {1} seconds".format(x, time.clock() - start)

如果我按原样运行代码(首先在foo中初始化sample_dict),然后在循环中再创建10次相同的字典,我会得到以下结果:
dict_init in Foo took 0.385263764287 sec
Run 0 took 0.548807949139 seconds
Run 1 took 0.533209452471 seconds
Run 2 took 0.51916067636 seconds
Run 3 took 0.513130722575 seconds
Run 4 took 0.508272050029 seconds
Run 5 took 0.502263872177 seconds
Run 6 took 0.48867601998 seconds
Run 7 took 0.483109299676 seconds
Run 8 took 0.479019713488 seconds
Run 9 took 0.473174195256 seconds
[Finished in 5.6s]

但是,如果我不初始化foo中的sample dict(commenting out foo.dict_init()),那么在循环中创建字典的速度会快20%。
Run 0 took 0.431378921359 seconds
Run 1 took 0.423696636179 seconds
Run 2 took 0.419630475616 seconds
Run 3 took 0.405130343806 seconds
Run 4 took 0.398099686921 seconds
Run 5 took 0.392837169802 seconds
Run 6 took 0.38799598399 seconds
Run 7 took 0.375133006408 seconds
Run 8 took 0.368755297573 seconds
Run 9 took 0.363273701371 seconds
[Finished in 4.0s]

我注意到,如果通过调用gc.disable()来关闭python的垃圾收集器,那么性能不仅提高了约5倍,而且在foo中存储大型字典也没有什么区别。
以下是禁用垃圾收集的结果:
dict_init in Foo took 0.0696136982496 sec
Run 0 took 0.113533445358 seconds
Run 1 took 0.111091241489 seconds
Run 2 took 0.111151620212 seconds
Run 3 took 0.110655722831 seconds
Run 4 took 0.111807537706 seconds
Run 5 took 0.11097510318 seconds
Run 6 took 0.110936170451 seconds
Run 7 took 0.111074414632 seconds
Run 8 took 0.110678488579 seconds
Run 9 took 0.111011066463 seconds

所以我有两个问题:
为什么禁用垃圾收集会加快字典的创建
如何在不禁用垃圾收集的情况下实现均匀性能(使用foo init和w/o)
我很感激你对这件事有任何见解。
谢谢您!
更新时间:
在TimPeters提到我正在创建可变对象之后,我决定尝试创建不可变对象(在我的例子中是元组)和voila-更快的结果(与使用GC和不使用GC相同)
dict_init in Foo took 0.017769 sec
Run 0 took 0.017547 seconds
Run 1 took 0.013234 seconds
Run 2 took 0.012791 seconds
Run 3 took 0.013371 seconds
Run 4 took 0.013288 seconds
Run 5 took 0.013692 seconds
Run 6 took 0.013059 seconds
Run 7 took 0.013311 seconds
Run 8 took 0.013343 seconds
Run 9 took 0.013675 seconds

我知道元组的创建比列表的创建快得多,但是为什么不可变对象的字典不会影响垃圾收集所花费的时间?不可变对象是否不包含在引用循环中?
谢谢您。
另外,在我的现实场景中,将列表转换为元组解决了这个问题(不需要列表,只是没有考虑使用元组),但我仍然好奇为什么列表更快。

最佳答案

“词典编纂”在这里真是一个难题。在这种情况下,字典创建所做的相关工作是创建十万个新的125元素列表。因为列表可以包含在引用循环中,这就创建了1250万个列表元素,每当CPython扫描包含dict的一代时,它就必须检查其循环垃圾收集。这些列表在字典中并不重要,但它们的存在却很重要。
所以,计时的主要内容是python的循环垃圾收集所消耗的时间。继续创建更多的dict并不特别重要,只重要的是创建新的可变对象(可能涉及到循环)的速度比销毁旧的可变对象快得多。这(分配超过释放)就是触发CPython循环GC的原因。
唉,你对此无能为力。通过在持续时间内禁用循环GC,可以使程序在创建新对象堆的清晰阶段中受益。不过,我猜不出这是否适用于你。
啊,忘了一件事:听写在“Foo里会有很大的不同,因为“Foo里的“粘在一起”。您创建的所有其他dict在构造之后都会立即被丢弃(cpython的引用计数负责这一点),因此不要增加循环GC所消耗的时间。但是,Foo中的dict是这样的,因为dict不会消失。更改循环以将新的dict追加到列表中,您将看到每个dict的时间都会增加,然后大量减少,然后再次增加,等等,这反映了dict在python的循环gc中向旧的代移动,因此循环gc扫描的频率会降低。它变得复杂;-)
更多细节?
更精确是很难的,因为在特定的情况下,循环GC的性能依赖于大量的实现细节,这些细节可以在不同的版本之间进行更改。
尽可能使用“不可变对象”的一般建议是基于对CPython中如何实现循环GC以及这些年来它是如何发展的一个相当深入的理解。
今天(最新版本的python 2和python 3),人们正竭尽全力将某些元组和dict从循环GC中排除。这可能会改变。特殊的外壳这样的东西是不免费的,所以决定是否添加更多像这样的技巧总是一个困难的平衡行为。对于不可变的对象来说,这是一个更容易的决定,因此建议朝着这些对象移动。
直到2008年底,tuples和dict才算是一个特例,详情请参见。
在一些罕见的案例中,结果是造成了可怕的性能后果,这些案例在2012年年中被修正为in this from the Python issue tracker
有一个好消息是添加了一个gc.is_tracked()函数,这样您至少可以研究循环GC是否要扫描特定对象。下面是Python2.7.5下的一些示例。不能保证他们总是这样工作:
“标量”对象(没有内部指针)从不被跟踪-它们不可能处于循环中:

>>> import gc
>>> gc.is_tracked(4)
False
>>> gc.is_tracked("2323")
False

元组最初被跟踪:
>>> t1 = ([1],)
>>> t2 = ((1.),)
>>> gc.is_tracked(t1), gc.is_tracked(t2)
(True, True)

但是,如果循环GC运行并确定一个元组“一直向下”是不可变的,那么它将解压该元组:
>>> gc.collect()
0
>>> gc.is_tracked(t1), gc.is_tracked(t2)
(True, False)

没有什么可以让它参与到一个循环中去,因为它和它的所有组件(on&on,一路向下)都是不可变的。但是仍然需要跟踪t2因为t1是可变的,t1[0]可能是以后循环的一部分:
>>> t1
([1],)
>>> t1[0][0] = t1
>>> t1
([([...],)],)

一个不同的策略恰好被用于听写。如果可能的话,它们是不加追踪的:
>>> d = {1: [2]}
>>> gc.is_tracked(d)
True

因为dict有一个可变的值,所以稍后它可能成为循环的一部分,所以必须由循环gc跟踪。
>>> d[1][0] = d
>>> d
{1: [{...}]}

但是有未跟踪的键和值的dict是未跟踪的:
>>> d = {1: 2}
>>> gc.is_tracked(d)
False

这是很棘手的,因为这样的听写会在以后成为一个循环的一部分!
>>> d[2] = d
>>> gc.is_tracked(d)
True

检测这种变化是不自由的。
然后是以上的组合:
>>> d = {(1, 2): (4, "abc", 5)}
>>> gc.is_tracked(d)
True
>>> gc.collect()
3
>>> gc.is_tracked(d)
False

在这种情况下,t1首先被跟踪,因为它的键和值(元组)首先被跟踪。但是在循环GC第一次运行之后,它确定键和值是“一路向下不可变的”,因此取消对dict的跟踪。
就像我说的,事情变得复杂;—)
顺便说一句,
我知道创建元组比创建列表快得多
创建列表的速度应该稍微慢一点。列表和元组在cpython中有非常相似的实现。元组需要较少的内存(对于非常短的序列来说,这可能是很重要的百分比),而且索引一个元组比索引一个列表要快一些。但是创造时间呢?它是一个类函数(对于元组)与两个类函数(对于列表)之间的区别,与元素的数量无关。这对于非常短的序列可能很重要,但对于大型序列则不重要。

关于python - 在Python中保留大型字典会影响应用程序性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19391648/

相关文章:

python - 在 Python 中拆分字符串时出现问题

scala - 修改 Scala 映射值

python - Python 3.6 中的多处理

php - 垃圾收集器在 PHP 中是如何工作的

python - 将值从 C++ 发送回 Python

python - Mac OS X 和 TeX Live 上 matplotlib 中的 TeX

python - python中有没有一种方法可以计算谷歌工作表中的一系列行(从第一行数据到最后一行数据)?

python - 为什么我的 python 字典总是有序的?

garbage-collection - Java GC 是确定性的

php - 长时间运行的 php 脚本的内存注意事项