如果参数是一个集合,为什么union会消耗更多内存?

时间:2013-03-04 09:14:18

标签: python memory-management set

我对set s:

的内存分配行为感到困惑
>>> set(range(1000)).__sizeof__()
32968
>>> set(range(1000)).union(range(1000)).__sizeof__()       # expected, set doesn't change
32968
>>> set(range(1000)).union(list(range(1000))).__sizeof__() #expected, set doesn't change
32968
>>> set(range(1000)).union(set(range(1000))).__sizeof__()  # not expected
65736

为什么使用set作为参数加倍结果set使用的内存量? 两种情况下的结果都与原始set

相同
>>> set(range(1000)) == set(range(1000)).union(range(1000)) == set(range(1000)).union(set(range(1000)))
True

请注意,使用普通迭代器会发生同样的情况:

>>> set(range(1000)).union(iter(list(range(1000)))).__sizeof__()
32968

使用update方法:

>>> a.update(range(1000))
>>> a.__sizeof__()
32968
>>> a.update(set(range(1000)))
>>> a.__sizeof__()
65736

起初我认为这是因为当调用union时,它会看到另一个set的大小为1000因此决定分配足够的内存以适应所有两个set的元素,但之后只使用部分内存,而在迭代器的情况下它只是迭代它并逐个添加元素(由于所有元素都是已经在set)。

range也是一个序列,第一个例子中的list也是如此。

>>> len(range(1000))
1000
>>> range(1000)[100]
100

那么为什么rangelist不会发生这种情况,而只会set? 这背后有任何设计决定,还是一个错误?


在Linux 64位上测试python 2.7.3和python 3.2.3。

1 个答案:

答案 0 :(得分:9)

在Python 2.7.3中,set.union()委托给名为set_update_internal()的C函数。后者根据其参数的Python类型使用几种不同的实现。这种多样化的实现解释了您所进行的测试之间的行为差​​异。

当参数为set时使用的实现在代码中记录了以下假设:

/* Do one big resize at the start, rather than
 * incrementally resizing as we insert new keys.  Expect
 * that there will be no (or few) overlapping keys.
 */

显然,在您的特定情况下,没有(或很少)重叠键的假设是不正确的。这就是导致最终set分配内存的原因。

我不确定我会称这是一个错误。 set的实施者选择了我看起来像是一个合理的权衡,而你只是发现自己处于这种权衡的错误一边。

权衡的好处是,在许多情况下,预分配会带来更好的表现:

In [20]: rhs = list(range(1000))

In [21]: %timeit set().union(rhs)
10000 loops, best of 3: 30 us per loop

In [22]: rhs = set(range(1000))

In [23]: %timeit set().union(rhs)
100000 loops, best of 3: 14 us per loop

在这里,set版本速度提高了一倍,大概是因为它不会重复分配内存,因为它会添加来自rhs的元素。

如果分配是一个交易破坏者,有很多方法可以解决它,其中一些你已经发现。