python - 为什么python for-loop中的临时变量计算占用这么多内存?

标签 python cpython

这个问题在这里已经有了答案:





Python string interning

(2 个回答)



About the changing id of an immutable string

(5 个回答)


2年前关闭。




下面两个代码是等价的,但是第一个大约需要700M内存,后一个只需要大约100M内存(通过windows任务管理器)。这里会发生什么?

def a():
    lst = []
    for i in range(10**7):
        t = "a"
        t = t * 2
        lst.append(t)
    return lst

_ = a()

def a():
    lst = []
    for i in range(10**7):
        t = "a" * 2
        lst.append(t)
    return lst

_ = a()

最佳答案

@vurmux 提出了不同内存使用的正确原因:字符串实习,但似乎缺少一些重要的细节。

CPython 实现在编译期间实习一些字符串,例如 "a"*2 - 有关如何/为什么的更多信息 "a"*2被拘留 看到这个 SO-post .

澄清:正如@MartijnPieters 在他的评论中正确指出的那样:重要的是编译器是否进行常量折叠(例如计算两个常量 "a"*2 的乘法)。如果完成常量折叠,则将使用生成的常量,并且列表中的所有元素都将引用同一个对象,否则不会。即使所有字符串常量都被实习(因此执行了常量折叠 => 字符串实习) - 谈论实习仍然是草率的:常量折叠是这里的关键,因为它也解释了根本没有实习的类型的行为,例如浮点数(如果我们使用 t=42*2.0 )。

是否发生了恒定折叠,可以通过 dis 轻松验证-module(我称你的第二个版本 a2() ):

>>> import dis
>>> dis.dis(a2)
  ...
  4          18 LOAD_CONST               2 ('aa')
             20 STORE_FAST               2 (t)
  ...

正如我们所看到的,在运行时不执行乘法,而是直接加载乘法的结果(在编译器时计算) - 结果列表包含对同一对象的引用(加载的常量) 18 LOAD_CONST 2):
>>> len({id(s) for s in a2()})
1

在那里,每个引用只需要 8 个字节,这意味着大约 80 Mb(+列表的过度分配+解释器所需的内存)需要内存。

在 Python3.7 中,如果结果字符串超过 4096 个字符,则不会执行常量折叠,因此替换 "a"*2"a"*4097导致以下字节码:
 >>> dis.dis(a1)
 ...
  4          18 LOAD_CONST               2 ('a')
             20 LOAD_CONST               3 (4097)
             22 BINARY_MULTIPLY
             24 STORE_FAST               2 (t)
 ...

现在,乘法不是预先计算的,结果字符串中的引用将是不同的对象。

优化器还不够聪明,无法识别 t实际上是 "a"t=t*2 ,否则它将能够执行常量折叠,但现在你的第一个版本的结果字节码(我称之为 a2() ):

...
5 22 LOAD_CONST 3 (2)
24 LOAD_FAST 2 (吨)
26 BINARY_MULTIPLY
28 STORE_FAST 2 (吨)
...

它将返回一个带有 10^7 的列表内部不同的对象(但所有对象都相等):
>>> len({id(s) for s in a1()})
10000000

即每个字符串需要大约 56 个字节(sys.getsizeof 返回 51,但由于 pymalloc-memory-allocator 是 8 字节对齐的,因此将浪费 5 个字节)+ 每个引用 8 个字节(假设 64 位 CPython 版本),因此关于610 Mb(+列表的过度分配+解释器所需的内存)。

您可以通过 sys.intern 强制执行字符串的实习。 :
import sys
def a1_interned():
    lst = []
    for i in range(10**7):
        t = "a"
        t = t * 2
        # here ensure, that the string-object gets interned
        # returned value is the interned version
        t = sys.intern(t) 
        lst.append(t)
    return lst

真的,我们现在不仅可以看到需要更少的内存,而且列表还引用了同一个对象(在线查看它的大小略小( 10^5 )here ):
>>> len({id(s) for s in a1_interned()})
1
>>> all((s=="aa" for s in a1_interned())
True

字符串实习可以节省大量内存,但有时很难理解字符串是否/为什么被实习。调用 sys.intern明确地消除了这种不确定性。
t 引用的其他临时对象的存在不是问题:CPython 使用引用计数进行内存管理,因此只要没有引用对象就会被删除 - 没有来自垃圾收集器的任何交互,垃圾收集器在 CPython 中仅用于分解循环(这是不同于例如 Java 的 GC,因为 Java 不使用引用计数)。因此,临时变量实际上是临时变量——这些对象不能累积以对内存使用产生任何影响。

临时变量t的问题只是它在编译过程中防止了窥孔优化,这是为"a"*2 执行的。但不适用于 t*2 .

关于python - 为什么python for-loop中的临时变量计算占用这么多内存?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57820593/

相关文章:

python - 我在哪里保存 .py 文件,以便我可以从 python 解释器(从 powershell )导入它

javascript - Django : How to give parameters to {% url %} tag in javascript function which will be used in FOR loop?

python - 在 rust-cpython 中将 Rust 结构转换为 PyObject

python - 切片端点被无形地截断

python - Python的代码对象是什么类型?

python - set() 是如何实现的?

python - lxml XPath 匹配 Python 中的值

python - Pandas Dataframe 在网页上显示

Python正则表达式避免匹配单词后跟多个条件

python - importlib.reload 是否应该在 Python 3.6 中恢复已删除的属性?