预分配无列表

时间:2014-03-06 13:14:37

标签: python performance list design-patterns python-3.x

假设您要编写一个产生对象列表的函数,并且您事先知道此列表的长度n

在python中,列表支持O(1)中的索引访问,因此可以预先分配列表并使用索引访问它而不是分配空列表并使用append()方法。这是因为如果空间不够,我们可以避免扩展整个列表的负担。

如果我正在使用python,那么在任何情况下表演都不是那么相关,但是预先分配列表的更好方法是什么?

我知道这些可能的候选人:

  • [None] * n→分配两个列表
  • [None for x in range(n)] - 或python2中的xrange→构建另一个对象

一个明显优于另一个吗?

如果我们处于n = len(input)的情况怎么办?由于input已经存在,[None for x in input]会有更好的表现吗? [None] * len(input)

3 个答案:

答案 0 :(得分:15)

当您将项目追加到列表中时,Python'过度分配',请参阅列表对象的source-code。这意味着,例如,当将1个项目添加到8个项目的列表中时,它实际上为8个新项目腾出空间,并且仅使用其中的第一个项目。接下来的7个追加是“免费”。

在许多语言(例如Matlab)中,总是告诉您需要预先分配向量,因为在循环期间追加非常昂贵。在最糟糕的情况下,将单个项目附加到长度为n的列表可能会花费O(n)时间,因为您可能需要创建更大的列表并复制所有现有项目。您需要在每次迭代时执行此操作,因此添加n项的总成本为O(n^2),哎哟。 Python的预分配方案通过许多单个附加扩展了阵列的成本(参见amortized costs),有效地降低了单个附加O(1)的成本以及添加n项的总成本O(n)

在Python中,其余代码的开销通常很大,通过预分配可以获得的微小加速是微不足道的。因此,在大多数情况下,只需忘记预分配,除非您的分析器告诉您附加到列表是瓶颈。

其他答案显示了列表预分配本身的一些分析,但这没用。唯一重要的是分析您的完整代码,在循环中进行所有计算,无论是否进行预分配。如果我的预测是正确的,那么差异是如此之小,以至于您赢得的计算时间与考虑,编写和维护额外行以预先分配列表所花费的时间相比相形见绌。

答案 1 :(得分:14)

在这两个选项之间,第一个显然更好,因为没有涉及Python for循环。

>>> %timeit [None] * 100
1000000 loops, best of 3: 469 ns per loop
>>> %timeit [None for x in range(100)] 
100000 loops, best of 3: 4.8 us per loop

<强>更新

list.append也有O(1) complexity,如果您将list.append方法分配给变量,那么它可能是比预创建列表更好的选择。

>>> n = 10**3
>>> %%timeit
lis = [None]*n           
for _ in range(n):
    lis[_] = _
... 
10000 loops, best of 3: 73.2 us per loop
>>> %%timeit
lis = []                 
for _ in range(n):
    lis.append(_)
... 
10000 loops, best of 3: 92.2 us per loop
>>> %%timeit
lis = [];app = lis.append
for _ in range(n):
    app(_)
... 
10000 loops, best of 3: 59.4 us per loop

>>> n = 10**6
>>> %%timeit
lis = [None]*n
for _ in range(n):
    lis[_] = _
... 
10 loops, best of 3: 106 ms per loop
>>> %%timeit
lis = []      
for _ in range(n):
    lis.append(_)
... 
10 loops, best of 3: 122 ms per loop
>>> %%timeit
lis = [];app = lis.append
for _ in range(n):
    app(_)
... 
10 loops, best of 3: 91.8 ms per loop

答案 2 :(得分:2)

显然,第一个版本。让我解释一下原因。

  1. 执行[None] * n时,Python会在内部创建一个大小为n的列表对象,并且复制同一个对象(此处为None) (这就是你应该在处理不可变对象时使用此方法)到所有内存位置的原因。所以内存分配只进行一次。之后,通过列表进行单次迭代,将对象复制到所有元素。 list_repeat是与此类列表创建相对应的函数。

    # Creates the list of specified size
    np = (PyListObject *) PyList_New(size);
    ....
    ...
    items = np->ob_item;
    if (Py_SIZE(a) == 1) {
        elem = a->ob_item[0];
        for (i = 0; i < n; i++) {
            items[i] = elem;       // Copies the same item
            Py_INCREF(elem);
        }
        return (PyObject *) np;
    }
    
  2. 当您使用列表推导来构建列表时,Python无法知道正在创建的列表的实际大小,因此它最初会分配一块内存和一个新副本对象存储在列表中。当列表超出分配的长度时,它必须再次分配内存并继续创建新对象并将其存储在列表中。

相关问题