Python 切片分配内存使用情况

Posted

技术标签:

【中文标题】Python 切片分配内存使用情况【英文标题】:Python Slice Assignment Memory Usage 【发布时间】:2011-06-24 07:31:42 【问题描述】:

我在 Stack Overflow 上的评论中读到,在更改列表时进行切片分配会提高内存效率。例如,

a[:] = [i + 6 for i in a]

应该比内存效率更高

a = [i + 6 for i in a]

因为前者替换现有列表中的元素,而后者创建一个新列表并将a 重新绑定到该新列表,将旧的a 留在内存中,直到它可以被垃圾回收。以两者速度为基准,后者稍快:

$ python -mtimeit -s 'a = [1, 2, 3]' 'a[:] = [i + 6 for i in a]'
1000000 loops, best of 3: 1.53 usec per loop
$ python -mtimeit -s 'a = [1, 2, 3]' 'a = [i + 6 for i in a]'
1000000 loops, best of 3: 1.37 usec per loop

这是我所期望的,因为重新绑定变量应该比替换列表中的元素更快。但是,我找不到任何支持内存使用声明的官方文档,而且我不确定如何对其进行基准测试。

从表面上看,内存使用声明对我来说是有道理的。但是,再考虑一下,我希望在前一种方法中,解释器会从列表推导中创建一个新列表,然后然后将该列表中的值复制到a,留下匿名列表浮动,直到它被垃圾收集。如果是这种情况,那么前一种方法将使用相同数量的内存,但速度也较慢。

谁能明确地展示(通过基准或官方文档)这两种方法中哪一种更节省内存/哪种是首选方法?

提前致谢。

【问题讨论】:

性能方面可能值得考虑,但我认为您更有可能遇到实际案例(在较大的程序中),您将引用传递给列表,例如从 Class1 到 Class2。在第一种情况下,使用切片赋值来修改 Class1 的列表将保留 Class2 的引用。在您引用的第二个实例中,修改 Class1 的列表意味着 Class2 将持有对不再有效的列表的引用。 @Brandon:这也是正确的,我可能应该在我的问题中提到区别。感谢您的意见。 【参考方案1】:

线

a[:] = [i + 6 for i in a]

不会节省任何内存。 Python 确实首先评估右侧,如language documentation 中所述:

赋值语句计算表达式列表(请记住,这可以是单个表达式或逗号分隔的列表,后者产生一个元组)并将单个结果对象从左到右分配给每个目标列表。

在本例中,单个结果对象将是一个新列表,而目标列表中的单个目标将是 a[:]

我们可以用生成器表达式替换列表推导:

a[:] = (i + 6 for i in a)

现在,右侧计算为生成器而不是列表。基准测试表明,这仍然比 naive 慢

a = [i + 6 for i in a]

那么生成器表达式是否真的节省了内存?乍一看,您可能认为确实如此。但深入研究source code of the function list_ass_slice() 表明事实并非如此。线

v_as_SF = PySequence_Fast(v, "can only assign an iterable");

首先使用PySequence_Fast() 将可迭代对象(在本例中为生成器)转换为元组,然后将其复制到旧列表中。元组使用与列表相同的内存量,因此在这种情况下使用生成器表达式与使用列表推导基本相同。在最后一个副本期间,原始列表的项目被重复使用。

道德似乎是,最简单的方法在任何方面都是最好的。

【讨论】:

+1 用于无情地粉碎过早的(内存)优化器。 感谢详细而有见地的回答!作为对上述评论者的回应,我想补充一点,如果您正在处理一个包含 500 万个元素的列表并且可以在复制它和不复制它之间进行选择,那么这可能不是过早的优化。 :) @Mitch:如果你有 500 万个条目,你可能会更好,例如一个 NumPy 数组而不是一个 Python 列表。 查看引擎盖下的机制使这个答案成为一个非常好的和明确的答案。 +1

以上是关于Python 切片分配内存使用情况的主要内容,如果未能解决你的问题,请参考以下文章

Cython:将单个元素分配给多维内存视图切片

Go探索-Slice

python中for循环的内存分配

【golang】内存逃逸常见情况和避免方式

8.5K Star! Python 代码内存分析的利器

Linux 进程分配的内存使用情况