【问题标题】:Pre-allocating a list of None预分配一个无列表
【发布时间】:2014-03-06 13:14:36
【问题描述】:

假设你想写一个函数来产生一个对象列表,并且你事先知道这个列表的长度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)?

【问题讨论】:

  • 你是对的,这不太可能是你的性能限制器。
  • 如果您遇到性能问题,请分析您的代码。瓶颈很少出现在您认为的位置。

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


【解决方案1】:

在这两个选项之间,第一个显然更好,因为不涉及 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

【讨论】:

  • 一些非常好的答案。我接受这个实际上提供了一些数据,以及将方法保存到变量中的有趣想法。也赞成其他答案。
  • 您使用_ 作为变量名有什么特殊原因吗?
  • @naught101 没有特别的原因。
【解决方案2】:

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

在许多语言中(例如,旧版本的 Matlab,新的 JIT 可能更好)你总是被告知你需要预先分配你的向量,因为在循环期间追加是非常昂贵的。在最坏的情况下,将单个项目附加到长度为 n 的列表可能会花费 O(n) 时间,因为您可能必须创建一个更大的列表并复制所有现有项目。如果您需要在每次迭代中执行此操作,添加 n 项目的总成本是 O(n^2),哎呀。 Python 的预分配方案将增加数组的成本分散到许多单个追加(请参阅amortized costs),有效地使单个追加的成本O(1) 和添加n 项目的总成本O(n)

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

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

【讨论】:

  • 源代码提到了增长模式0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...,所以它可能是二次的(或者你怎么称呼它),而不是指数的。
  • 那是我的误解......不记得我在哪里读到的。我会删除评论。
  • 它是指数级的,只是一个非常慢的。主要的增长率是 (1.125)^k,但增长往往受 cmets 所示序列的固定贡献 (+3/+6) 支配。
【解决方案3】:

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

  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 无法知道正在创建的列表的实际大小,因此它最初会分配一块内存和一个 新副本对象存储在列表中。当列表超出分配的长度时,它必须再次分配内存并继续创建新对象并将其存储在列表中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-09
    • 2012-06-24
    • 2020-04-25
    • 1970-01-01
    • 1970-01-01
    • 2020-05-04
    • 1970-01-01
    • 2017-10-29
    相关资源
    最近更新 更多