Python内存模型

标签 python arrays memory model

我有一个很大的 list
假设我这样做了(是的,我知道代码非常不符合 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 中,第二个选项确实更节省内存......

有没有人对此有解释?

最佳答案

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被重新分配,现在指的是不同的对象,而 a 中的两个条目还是引用上一篇。

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

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

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

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

It's weird, though - after all, how is the reference to the integer stored? id(variable) gives an integer, the reference is an integer itself, isn't it cheaper to use the integer?



CPython 将引用存储为 PyObject 的指针(用 Java 和 C# 编写的 Jython 和 IronPython 使用这些语言的隐式引用;用 Python 编写的 PyPy 具有非常灵活的后端,可以使用许多不同的策略)
id(v)给出(仅在 CPython 上)指针的数值(作为唯一标识对象的便捷方式)。列表可以是异构的(有些项目可能是整数,其他项目可能是不同类型的对象),因此将某些项目存储为指向 PyObject 的指针和其他不同的项目并不是一个明智的选择(每个对象也需要一个类型指示,并且在 CPython 中,引用计数,至少) -- array.array是同质且有限的,因此它确实可以(并且确实)存储项目值的副本而不是引用(这通常更便宜,但不适用于同一项目出现很多的集合,例如稀疏数组,其中绝大多数项数为 0)。

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

Also, would it make a difference if I assigned 2**64 to every slot instead of assigning n, when n holds a reference to 2**64? What happens when I just write 1?



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

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

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

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

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

关于Python内存模型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1059674/

相关文章:

python - 为什么在使用 python 和 numpy 时 sin(180) 不为零?

python - 如何创建一个全新的(未安装任何软件包)conda 环境?

python - numpy 的性能是否因操作系统而异?

python - 在日期时间列中查找日期名称,用户输入的工作日名称为 'String'

python - 在 PyPy 下使用 __slots__

java - 如何将多个重复的json数组排序为不重复的字符串数组?

javascript - 在 React Native 中更新嵌套状态对象?

c++ - 如何在 C++ 中写入二维数组?

linux - centOS 和 Virtualbox 中的内存增加

c - 在 C 中使用 malloc 进行内存分配