有没有办法绕过Python list.append()随着列表的增长逐渐变慢?

时间:2010-03-18 22:30:08

标签: python class list performance append

我正在阅读一个大文件,并将每几行转换为一个Object实例。

由于我循环遍历文件,因此我使用list.append(instance)将实例存储到列表中,然后继续循环。

这是一个大约100MB左右的文件,所以它不是太大,但随着列表变大,循环逐渐减慢。 (我打印循环中每圈的时间。)

这不是循环所固有的〜当我在循环浏览文件时打印每个新实例时,程序以恒定速度进行〜只有当我将它们附加到列表时它才会变慢。

我的朋友建议在while循环之前禁用垃圾收集并在之后启用它&进行垃圾收集调用。

有没有其他人观察到类似的问题,list.append越来越慢?有没有其他方法来规避这个?


我将尝试以下两项建议。

(1)“预先分配”记忆〜最好的方法是什么? (2)尝试使用deque

多个帖子(请参阅Alex Martelli的评论)建议内存碎片化(他有像我这样的大量可用内存)〜但没有明显的性能修复。

要复制这种现象,请在答案中运行下面提供的测试代码,并假设这些列表包含有用的数据。


gc.disable()和gc.enable()有助于计时。我还会仔细分析所有时间花在哪里。

6 个答案:

答案 0 :(得分:91)

您观察到的性能不佳是由您正在使用的版本中的Python垃圾收集器中的错误引起的。升级到Python 2.7或3.1或更高版本以重新获得amoritized 0(1)行为期望列表附加在Python中。

如果无法升级,请在构建列表时禁用垃圾回收,并在完成后将其打开。

(您还可以调整垃圾收集器的触发器或在您进行时有选择地调用collect,但我不会在这个答案中探索这些选项,因为它们更复杂,我怀疑您的用例适合上述解决方案。)< / p>

背景:

请参阅:https://bugs.python.org/issue4074以及https://docs.python.org/release/2.5.2/lib/module-gc.html

记者观察到,随着列表长度的增加,将复杂对象(非数字或字符串的对象)附加到列表会线性减慢。

此行为的原因是垃圾收集器正在检查并重新检查列表中的每个对象,以查看它们是否有资格进行垃圾回收。此行为导致将对象添加到列表的时间线性增加。修复程序预计将在py3k中登陆,因此它不应该适用于您正在使用的解释程序。

测试:

我做了一个测试来证明这一点。对于1k次迭代,我将10k对象附加到列表中,并记录每次迭代的运行时。整体运行时差异很明显。在测试的内部循环期间禁用垃圾收集,我的系统上的运行时间为18.6秒。在整个测试中启用垃圾收集后,运行时间为899.4秒。

这是测试:

import time
import gc

class A:
    def __init__(self):
        self.x = 1
        self.y = 2
        self.why = 'no reason'

def time_to_append(size, append_list, item_gen):
    t0 = time.time()
    for i in xrange(0, size):
        append_list.append(item_gen())
    return time.time() - t0

def test():
    x = []
    count = 10000
    for i in xrange(0,1000):
        print len(x), time_to_append(count, x, lambda: A())

def test_nogc():
    x = []
    count = 10000
    for i in xrange(0,1000):
        gc.disable()
        print len(x), time_to_append(count, x, lambda: A())
        gc.enable()

完整来源:https://hypervolu.me/~erik/programming/python_lists/listtest.py.txt

图形结果:红色表示gc打开,蓝色表示gc关闭。 y轴是以对数方式缩放的秒数。

http://hypervolu.me/~erik/programming/python_lists/gc.png

由于两个图在y分量中相差几个数量级,因此它们独立地与y轴线性缩放。

http://hypervolu.me/~erik/programming/python_lists/gc_on.png

http://hypervolu.me/~erik/programming/python_lists/gc_off.png

有趣的是,关闭垃圾回收,我们发现每10k附加的运行时只有小的峰值,这表明Python的列表重新分配成本相对较低。无论如何,它们比垃圾收集成本低许多个数量级。

上述图的密度使得很难看到垃圾收集器开启时,大多数间隔实际上具有良好的性能;只有当垃圾收集器循环时才会遇到病理行为。您可以在10k附加时间的直方图中观察到这一点。大多数数据点每10k附加量下降0.02s左右。

http://hypervolu.me/~erik/programming/python_lists/gc_on.hist.png

用于生成这些图的原始数据可以在http://hypervolu.me/~erik/programming/python_lists/

找到

答案 1 :(得分:14)

没有什么可以规避的:追加到列表是O(1)摊销。

列表(在CPython中)是一个数组,至少与列表一样长,最多两倍。如果数组未满,则附加到列表就像分配其中一个数组成员(O(1))一样简单。每次数组都满了,它的大小会自动加倍。这意味着有时需要O(n)操作,但只需要每n次操作,并且随着列表变大,它越来越少需要。 O(n)/ n ==&gt; O(1)。 (在其他实现中,名称和详细信息可能会发生变化,但必须保持相同的时间属性。)

附加到列表已经扩展。

当文件变得很大时,您是否有可能无法将所有内容保存在内存中并且您遇到操作系统分页到磁盘的问题?它是否可能是你的算法的另一部分不能很好地扩展?

答案 2 :(得分:6)

很多这些答案都是疯狂的猜测。我喜欢Mike Graham是最好的,因为他对列表的实现是正确的。但是我已经写了一些代码来重现你的主张并进一步研究它。以下是一些调查结果。

这就是我的开始。

import time
x = []
for i in range(100):
    start = time.clock()
    for j in range(100000):
        x.append([])
    end = time.clock()
    print end - start

我只是将空列表附加到列表x。我打印出每100,000次附加的持续时间,100次。它确实像你声称的那样减速。 (第一次迭代为0.03秒,最后一次为0.84秒......相当不同。)

显然,如果您实例化一个列表但不将其附加到x,它会更快地运行,并且不会随着时间的推移而扩展。

但如果您将x.append([])更改为x.append('hello world'),则根本没有速度增加。同一个对象被添加到列表100 * 100,000次。

我对此做了什么:

  • 速度降低与列表大小无关。它与实时Python对象的数量有关。
  • 如果您根本没有将这些项目附加到列表中,他们只会立即收集垃圾,不再由Python管理。
  • 如果反复追加相同的项目,则实时Python对象的数量不会增加。但该列表必须每隔一段时间重新调整一次。但这不是性能问题的根源。
  • 由于您正在创建大量新创建的对象并将其添加到列表中,因此它们仍处于活动状态且不会被垃圾回收。减速可能与此有关。

就可以解释这一点的Python内部而言,我不确定。但我很确定列表数据结构不是罪魁祸首。

答案 3 :(得分:1)

您可以尝试 http://docs.python.org/release/2.5.2/lib/deque-objects.html 在列表中分配预期数量的必需元素吗? ?我敢打赌,该列表是一个连续的存储,必须每隔几次迭代重新分配和复制。 (类似于c ++中std :: vector的一些流行实现)

编辑:由http://www.python.org/doc/faq/general/#how-are-lists-implemented备份

答案 4 :(得分:1)

我在使用Numpy数组时遇到了这个问题,如下所示:

import numpy
theArray = array([],dtype='int32')

随着阵列的增长,在一个循环中追加到这个数组的时间越来越长,这是一个交易中断,因为我有14M的附加值。

上面概述的垃圾收集器解决方案听起来很有希望,但没有用。

使用预定义大小创建数组的工作原理如下:

theArray = array(arange(limit),dtype='int32')

确保限制大于您需要的数组。

然后,您可以直接在数组中设置每个元素:

theArray[i] = val_i

最后,如有必要,可以删除数组中未使用的部分

theArray = theArray[:i]

这在我的案例中有很大的不同。

答案 5 :(得分:1)

使用集合,然后将其转换为最后的列表

function processJSON(data) {
    var json = JSON.parse(data);
   //..
}

我有同样的问题,这解决了几个订单的时间问题。