list() 使用比列表理解稍多的内存
Posted
技术标签:
【中文标题】list() 使用比列表理解稍多的内存【英文标题】:list() uses slightly more memory than list comprehension 【发布时间】:2017-02-22 10:10:38 【问题描述】:所以我在玩list
对象,发现如果list
是用list()
创建的,它使用的内存比列表理解更多?我正在使用 Python 3.5.2
In [1]: import sys
In [2]: a = list(range(100))
In [3]: sys.getsizeof(a)
Out[3]: 1008
In [4]: b = [i for i in range(100)]
In [5]: sys.getsizeof(b)
Out[5]: 912
In [6]: type(a) == type(b)
Out[6]: True
In [7]: a == b
Out[7]: True
In [8]: sys.getsizeof(list(b))
Out[8]: 1008
来自docs:
可以通过多种方式构建列表:
使用一对方括号来表示空列表:[]
使用方括号,用逗号分隔项目:[a]
,[a, b, c]
使用列表理解:[x for x in iterable]
使用类型构造函数:list()
或list(iterable)
但似乎使用list()
会占用更多内存。
list
越大,差距越大。
为什么会这样?
更新 #1
使用 Python 3.6.0b2 进行测试:
Python 3.6.0b2 (default, Oct 11 2016, 11:52:53)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(range(100)))
1008
>>> sys.getsizeof([i for i in range(100)])
912
更新 #2
使用 Python 2.7.12 进行测试:
Python 2.7.12 (default, Jul 1 2016, 15:12:24)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(xrange(100)))
1016
>>> sys.getsizeof([i for i in xrange(100)])
920
【问题讨论】:
这是一个非常有趣的问题。我可以在 Python 3.4.3 中重现该现象。更有趣的是:在 Python 2.7.5 上,sys.getsizeof(list(range(100)))
是 1016,getsizeof(range(100))
是 872,getsizeof([i for i in range(100)])
是 920。所有类型都具有 list
。
有趣的是,这种差异在 Python 2.7.10 中也存在(尽管实际数字与 Python 3 不同)。 3.5 和 3.6b 中也有。
我在 Python 2.7.6 上得到的数字与 @SvenFestersen 相同,在使用 xrange
时也是如此。
这里有一个可能的解释:***.com/questions/7247298/size-of-list-in-memory。如果其中一种方法使用append()
创建列表,则可能存在内存过度分配。我想真正澄清这一点的唯一方法是查看 Python 源代码。
仅多出 10%(你不会真的在任何地方这么说)。我将标题改写为“稍微多一点”。
【参考方案1】:
我认为您看到的是过度分配模式,这是sample from the source:
/* This over-allocates proportional to the list size, making room
* for additional growth. The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
* sequence of appends() in the presence of a poorly-performing
* system realloc().
* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
*/
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
打印长度为 0-88 的列表推导的大小,您可以看到模式匹配:
# create comprehensions for sizes 0-88
comprehensions = [sys.getsizeof([1 for _ in range(l)]) for l in range(90)]
# only take those that resulted in growth compared to previous length
steps = zip(comprehensions, comprehensions[1:])
growths = [x for x in list(enumerate(steps)) if x[1][0] != x[1][1]]
# print the results:
for growth in growths:
print(growth)
结果(格式为(list length, (old total size, new total size))
):
(0, (64, 96))
(4, (96, 128))
(8, (128, 192))
(16, (192, 264))
(25, (264, 344))
(35, (344, 432))
(46, (432, 528))
(58, (528, 640))
(72, (640, 768))
(88, (768, 912))
过度分配是出于性能原因,允许列表增长,而无需为每次增长分配更多内存(更好的amortized 性能)。
与使用列表推导不同的一个可能原因是列表推导无法确定性地计算生成列表的大小,但list()
可以。这意味着推导会在使用过度分配填充列表时不断增长列表,直到最终填充它。
一旦完成,它可能不会增加未使用分配节点的过度分配缓冲区(事实上,在大多数情况下不会,这会破坏过度分配的目的)。
list()
但是,无论列表大小如何,都可以添加一些缓冲区,因为它提前知道最终列表大小。
另一个支持证据,同样来自源,是我们看到list comprehensions invoking LIST_APPEND
,这表明list.resize
的使用,这反过来表明在不知道将填充多少的情况下消耗了预分配缓冲区。这与您看到的行为一致。
总之,list()
将根据列表大小预分配更多节点
>>> sys.getsizeof(list([1,2,3]))
60
>>> sys.getsizeof(list([1,2,3,4]))
64
列表推导不知道列表的大小,因此它在增长时使用追加操作,从而耗尽预分配缓冲区:
# one item before filling pre-allocation buffer completely
>>> sys.getsizeof([i for i in [1,2,3]])
52
# fills pre-allocation buffer completely
# note that size did not change, we still have buffered unused nodes
>>> sys.getsizeof([i for i in [1,2,3,4]])
52
# grows pre-allocation buffer
>>> sys.getsizeof([i for i in [1,2,3,4,5]])
68
【讨论】:
但是为什么过度分配会发生在一个而不是另一个? 这具体来自list.resize
。我不是浏览源代码的专家,但如果一个调用调整大小而另一个没有调用 - 它可以解释差异。
这里是 Python 3.5.2。尝试循环打印 0 到 35 的列表大小。对于列表,我看到 64, 96, 104, 112, 120, 128, 136, 144, 160, 192, 200, 208, 216, 224, 232, 240, 256, 264, 272, 280, 288, 296, 304, 312, 328, 336, 344, 352, 360, 368, 376, 384, 400, 408, 416
和理解 64, 96, 96, 96, 96, 128, 128, 128, 128, 192, 192, 192, 192, 192, 192, 192, 192, 264, 264, 264, 264, 264, 264, 264, 264, 264, 344, 344, 344, 344, 344, 344, 344, 344, 344
。除了理解是似乎预先分配内存的算法之外,我会使用更多 RAM 用于某些大小的算法。
我也希望如此。我很快就能进一步研究它。好的cmets。
实际上list()
确定性地确定列表大小,而列表理解无法做到这一点。这表明列表理解并不总是“触发”列表的“最后”增长。可能有道理。【参考方案2】:
感谢大家帮助我理解了很棒的 Python。
我不想提出那么大的问题(这就是我发布答案的原因),只想展示和分享我的想法。
正如@ReutSharabani 正确指出的那样:“list() 确定性地确定列表大小”。您可以从该图表中看到它。
当您append
或使用列表推导时,您总是有某种边界,当您到达某个点时会扩展。而list()
的边界几乎相同,但它们是浮动的。
更新
感谢@ReutSharabani、@tavo、@SvenFestersen
总结一下:list()
预分配内存取决于列表大小,列表理解无法做到这一点(它在需要时请求更多内存,例如 .append()
)。这就是list()
存储更多内存的原因。
还有一张图表,显示list()
预分配内存。所以绿线显示list(range(830))
逐个元素附加,并且一段时间内存没有改变。
更新 2
正如@Barmar 在下面的 cmets 中指出的,list()
必须比列表理解更快,所以我运行 timeit()
和 number=1000
的长度从 list
从 4**0
到 4**10
并且结果是
【讨论】:
红线高于蓝线的答案是,当list
构造函数可以从它的参数中确定新列表的大小时,它仍然会预先分配与最后一个元素相同的空间量到了那里,没有足够的空间放它。至少这对我来说是有意义的。
@tavo 这对我来说似乎是一样的,过了一会儿我想在图表中显示它。
因此,虽然列表推导使用较少的内存,但由于发生了所有调整大小,它们可能会明显变慢。这些通常必须将列表主干复制到新的内存区域。
@Barmar 实际上我可以使用range
对象进行一些时间测量(这可能很有趣)。
它会让你的图表更漂亮。 :)以上是关于list() 使用比列表理解稍多的内存的主要内容,如果未能解决你的问题,请参考以下文章