这个问题在这里已经有了答案:
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/