list()比列表理解使用更多的内存


79

因此,我在玩list对象时发现一点奇怪的事情:如果listlist()它创建的东西比列表理解力要占用更多的内存?我正在使用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

文档

列表可以通过几种方式构造:

  • 使用一对方括号表示空白列表: []
  • 使用方括号,以逗号分隔项目:[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

3
这是一个非常有趣的问题。我可以在Python 3.4.3中重现该现象。更有趣的是:在Python 2.7.5sys.getsizeof(list(range(100)))getsizeof(range(100))是1016、872和getsizeof([i for i in range(100)])920。所有类型都具有list
斯文·费斯特森

有趣的是,Python 2.7.10中也存在这种差异(尽管实际数字与Python 3不同)。在3.5和3.6b中也有。
cdarke '16

在使用时,Python 2.7.6与@SvenFestersen的编号相同xrange
RemcoGerlich '16

2
这里有一个可能的解释:stackoverflow.com/questions/7247298/size-of-list-in-memory。如果其中一种方法使用创建列表append(),则可能存在内存过度分配。我想真正弄清楚这一点的唯一方法是看看Python源代码。
Sven Festersen '16

仅增加10%(您在任何地方都不会说)。我将标题改成“稍微多一点”。
smci

Answers:


61

我认为您正在看到过度分配模式,这是来自源示例


打印长度为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))

出于性能原因而进行了超额分配,从而允许列表增长而不会每次增长都分配更多内存(更好的摊销性能)。

使用列表理解的差异的一个可能原因是列表理解不能确定性地计算所生成列表的大小,但是list()可以。这意味着,在使用过度分配填充列表的过程中,理解力将不断增长,直到最终填充它。

一旦完成,有可能不会使用未分配的分配节点来增加过度分配缓冲区(实际上,在大多数情况下,这样做不会克服过度分配的目的)。

list()但是,无论列表大小如何,都可以添加一些缓冲区,因为它事先知道最终的列表大小。


同样从源头获得的另一个支持证据是,我们看到列表理解正在调用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

4
但是,为什么过多分配会发生在一个而不是另一个上呢?
cdarke

这具体是来自list.resize。我不是浏览其来源的专家,但是如果一个调用调整大小而另一个调用不调整大小,则可以解释其中的区别。
Reut Sharabani

6
此处的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的算法。
塔沃

我也希望如此。我可以尽快进一步研究。好评论。
Reut Sharabani

4
实际上是list()确定性地确定列表大小,而列表理解无法做到。这表明列表理解并不总是“触发”列表的“最后”增长。可能有道理。
Reut Sharabani

30

感谢每个人帮助我理解出色的Python。

我不想提出这么大的问题(为什么我要发布答案),只想展示和分享我的想法。

正如@ReutSharabani正确指出的那样:“ list()确定性地确定列表大小”。您可以从该图中看到它。

尺寸图

当您append使用列表推导或使用列表推导时,您总是会遇到某种界限,直到您到达某个点。与list()您有几乎相同的界限,但是界限是浮动的。

更新

因此,感谢@ReutSharabani@tavo@SvenFestersen

总结一下:list()根据列表大小预分配内存,列表理解不能做到这一点(它需要时会请求更多的内存,例如.append())。这就是为什么要list()存储更多的内存。

list()另一幅图显示了预分配的内存。因此,绿线显示list(range(830))逐个元素追加,一段时间后内存没有变化。

list()预分配内存

更新2

正如@Barmar在下面的评论中指出的,list()我一定比列表理解要快,所以我跑timeit()了从到的number=1000长度,结果是list4**04**10

时间测量


1
为什么红线位于蓝色上方的答案是,当list构造函数可以根据其参数确定新列表的大小时,它仍将预分配与最后一个元素到达那里且没有足够空间的空间相同的空间。至少那对我有意义。
塔沃

@tavo对我来说似乎相同,过一会儿我想在图表中显示它。
vishes_shell

2
因此,尽管列表推导使用较少的内存,但由于发生了所有调整大小,因此它们可能会明显变慢。这些通常将必须将列表主干复制到新的内存区域。
Barmar

@Barmar实际上我可以用range对象运行一些时间测量(这可能很有趣)。
vishes_shell

它将使您的图表更加美观。:)
Barmar
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.