【问题标题】:Python Memory ModelPython 内存模型
【发布时间】:2010-11-06 18:39:05
【问题描述】:

我有一个很大的列表 假设我这样做(是的,我知道代码非常不符合 Python 标准,但为了示例的缘故..):

n = (2**32)**2
for i in xrange(10**7)
  li[i] = n

工作正常。但是:

for i in xrange(10**7)
  li[i] = i**2

消耗大量内存。我不明白为什么会这样 - 存储大数字需要更多位,而在 Java 中,第二个选项确实更节省内存......

有人对此有解释吗?

【问题讨论】:

  • 另外,我认为您可以通过将 range() 更改为 xrange() 来使用更少的内存。 xrange 使用迭代器而不是生成整个列表,因此使用更少的内存。 (或者至少我是这么理解的)
  • 根据 cmets 对答案的混淆,我不认为你的意思是你所说的,@unknown - 一个数组(导入数组,并使用 array.array)不能保存整数> 32 位;我认为您的意思是一个列表-请编辑问题以澄清,因为它确实有很大的不同!-) [使用 array.array 您不会观察到您所描述的内存消耗行为等]。
  • 在 Python 3 中 range() 做了 xrange() 以前做的事情,所以它取决于 Python 的版本。
  • @Mike,虽然你是对的,但这并不能说明内存使用量的差异

标签: python arrays memory model


【解决方案1】:

你只有一个变量 n,但你创建了许多 i**2。

发生的情况是 Python 使用引用。每次执行array[i] = n 时,都会创建一个对n 值的新引用。请注意,不是变量,而是值。但是,在第二种情况下,当您执行 array[i] = i**2 时,您会创建一个新值,并引用这个新值。这当然会占用更多的内存。

事实上,Python 将继续重复使用相同的值,并且即使重新计算,也只会使用对它的引用。比如:

l = []
x = 2
for i in xrange(1000000):
    l.append(x*2)

通常不会使用比

更多的内存
l = []
x = 2
for i in xrange(1000000):
    l.append(x)

但是,在

的情况下
l = []
x = 2
for i in xrange(1000000):
    l.append(i)

i 的每个值都会得到一个引用,因此会保存在内存中,与其他示例相比会占用大量内存。

(Alex 指出了一些术语上的混乱。在 python 中有一个名为 array 的模块。这些类型的数组存储整数值,而不是像 Python 的普通列表对象那样对对象的引用,但在其他方面表现相同。但自从第一个示例使用了无法存储在此类数组中的值,此处不太可能出现这种情况。

相反,问题很可能使用了许多其他语言中使用的单词数组,这与 Python 的列表类型相同。)

【讨论】:

  • 这并没有考虑到更大的内存使用情况。
  • 是的。在第二种情况下,创建了 i**2 的 len(array) 个数。他们耗尽了内存。在第一种情况下,只有一个 n。这不会占用太多内存。
  • Python 将为每个对“n”的引用创建一个新的整数对象。
  • @Matthew,如果 arr 是一个 array.array,是的,那么它存储值的副本(所以,@Lennart,它并不总是通过引用——array.array 是一个特殊情况单个小类型的值的副本,这就是为什么它通常比列表紧凑得多)。特别是 array.array 无法存储超过 32 位的整数,所以我们大多都假设 OP 写错了,当他真正的意思是列表时说 array,而不是 array.array!
  • @Lennart - 既然您已经获得了成功的答案(目前),为什么不解释一下并澄清一个似乎相当普遍的误解?
【解决方案2】:

在您的第一个示例中,您存储了相同的整数 len(arr) 次。所以python只需要在内存中存储一​​次整数并引用它len(arr)次。

在您的第二个示例中,您存储 len(arr) 不同的整数。现在 python 必须为 len(arr) 整数分配存储空间,并在每个 len(arr) 槽中引用它们。

【讨论】:

  • 那么python本质上是像存储和引用一个对象一样存储和引用一个整数吗?
  • Matthew,Python 将值“单独存储在每个元素中”。这不是 C,而是 Python。
  • @Matt - 我站在你这边,但我们错了。在解释器中使用 id() 函数的时间为我清除了一切。,
  • 感谢您的澄清,并对我的错误解释感到抱歉。
【解决方案3】:

Java 对一些值类型(包括整数)进行了特殊处理,以便它们按值存储(而不是像其他所有内容一样通过对象引用)。 Python 不会对此类类型进行特殊处理,因此将 n 分配给 列表中的许多条目(或其他普通 Python 容器)不必进行复制。

编辑:请注意,引用总是对对象,而不是“对变量”——在 Python(或 Java)中没有“对变量的引用”之类的东西。例如:

>>> n = 23
>>> a = [n,n]
>>> print id(n), id(a[0]), id(a[1])
8402048 8402048 8402048
>>> n = 45
>>> print id(n), id(a[0]), id(a[1])
8401784 8402048 8402048

我们从第一个打印中看到,列表 a 中的两个条目都引用了与 n 所引用的完全相同的对象 -- 但是当重新分配 n 时,it 现在引用一个不同的对象,而a 中的两个条目仍然引用前一个。

array.array(来自 Python 标准库模块 array)与列表非常不同:它保留同质类型的紧凑副本,每个项目占用的位数与存储该类型值的副本所需的位数一样少类型。所有普通容器都保留引用(在 C 编码的 Python 运行时内部实现为指向 PyObject 结构的指针:在 32 位构建中,每个指针占用 4 个字节,每个 PyObject 至少 16 个左右[包括指向类型的指针、引用计数, 实际值和 malloc 向上取整]),数组没有(因此它们不能是异构的,除了少数基本类型之外不能有项目等)。

例如,一个包含 1000 个项目的容器,所有项目都是不同的小整数(每个项目的值可以容纳 2 个字节),将大约 2,000 个字节的数据作为 array.array('h'),但大约 20,000 个字节作为 @ 987654329@。但是,如果所有项目都是相同的数字,则数组仍将占用 2,000 个字节的数据,列表将仅占用 20 个左右 [[在每种情况下,您都必须为容器对象添加大约 16 或 32 个字节正确的,除了数据的内存]]。

然而,虽然问题是“数组”(即使在标签中),但我怀疑它的 arr 实际上是一个数组——如果是,它不能存储 (2**32)*2 (最大 int数组中的值是 32 位),并且实际上不会观察到问题中报告的内存行为。所以,这个问题实际上可能是关于一个列表,而不是一个数组。

编辑:@ooboo 的评论提出了许多合理的后续问题,而不是试图压缩评论中的详细解释,我将其移至此处。

不过,这很奇怪——毕竟,这是怎么回事? 对存储的整数的引用? id(variable) 给出一个整数,即 参考本身是一个整数,不是 使用整数更便宜?

CPython 将引用存储为指向 PyObject 的指针(用 Java 和 C# 编写的 Jython 和 IronPython 使用这些语言的隐式引用;用 Python 编写的 PyPy 具有非常灵活的后端,可以使用许多不同的策略)

id(v) 给出(仅在 CPython 上)指针的数值(作为唯一标识对象的便捷方式)。列表可以是异构的(一些项目可能是整数,其他对象可能是不同类型的),因此将一些项目存储为指向 PyObject 和其他不同的指针并不是一个明智的选择(每个对象还需要一个类型指示,并且在 CPython 中,一个至少引用计数)——array.array 是同质且有限的,因此它可以(并且确实)存储项目值的副本而不是引用(这通常更便宜,但不适用于相同项目出现的集合很多,例如一个稀疏数组,其中绝大多数项为 0)。

语言规范完全允许 ​​Python 实现尝试更微妙的优化技巧,只要它保持语义不变,但据我所知,目前没有针对此特定问题的(您可以尝试破解 PyPy 后端,但如果检查 int 与非 int 的开销超过了预期的收益,请不要感到惊讶。

另外,如果我这样做会有所不同吗? 将2**64 分配给每个插槽 分配 n,当 n 持有 a 参考2**64?什么时候发生 我只写1?

这些是完全允许每个实现做出的实现选择示例,因为保留语义并不难(因此假设,即使 3.1 和 3.2 在这方面表现不同)。

当您使用 int 字面量(或任何其他不可变类型的字面量)或其他产生此类结果的表达式时,由实现决定是否无条件地创建该类型的新对象,或者花一些时间检查这些对象,看看是否有一个可以重复使用的现有对象。

在实践中,CPython(我相信其他实现,但我不太熟悉它们的内部结构)使用足够 small 整数的单个副本(保留几个小的预定义 C 数组PyObject 形式的整数值,可随时使用或在需要时重用),但通常不会特意寻找其他现有的可重用对象。

但是例如,同一函数中相同的字面常量很容易编译为对函数常量表中单个常量对象的引用,所以这是一个很容易完成的优化,我相信每个当前的 Python 实现都可以执行它。

有时很难记住 Python 是一种一门语言,它有几个实现可能(合法且正确地)在很多细节上有所不同——每个人,包括像我这样的学究,在谈论流行的 C 编码实现时,倾向于只说“Python”而不是“CPython”(在这种情况下,区分语言和实现是最重要的;-)。不过,区别非常重要,值得不时重复一遍。

【讨论】:

  • 这不可能。以下代码不会反驳您的答案:n=5; a=[n,n]; n=3;打印一个;如果 a 确实存储了对 n 的引用,则应该打印 [3, 3]。它打印 [5,5]
  • 它存储对 n 在构建 a 时引用的同一对象的引用;然后您切换名称 n 以引用不同的对象。它总是引用 TO OBJECTS(这就是我说“对象引用”的原因,你知道!-),NEVER 引用 TO NAMES,这似乎成为你所混淆的东西(在 Pythor 或 Java 中没有对名称的引用,只有对对象的引用)。
  • 不,因为 n 也是对值 5 的引用。都是引用。
  • 是的,我现在看到了区别。很有趣。
  • @Triptych:整数是不可变的对象,你不能改变它们的值。变量“n”只是一个对象的标签(最初是不可变整数 5,后来是整数 3)。
【解决方案4】:

在这两个示例中,arr[i] 都引用了对象,无论它是 n 还是 i * 2 的结果对象。

在第一个示例中,n 已经定义,因此它只需要引用,但在第二个示例中,它必须评估 i * 2,如果需要,GC 必须为这个新结果对象分配空间,然后使用它的引用.

【讨论】:

    猜你喜欢
    • 2014-05-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-07
    • 2011-02-28
    • 2012-08-11
    • 2011-06-03
    相关资源
    最近更新 更多