如果一个函数在循环中创建了很多对象,这些对象在其他地方没有被引用,由于 Python 的引用计数,它们将被立即删除。 如果我这次将对象存储在一个列表中,那么当函数退出时,这些对象将被垃圾回收删除。这些对象没有循环引用。为什么当函数结束并删除列表时,它们没有被 ReferenceCounting 删除? 作为示例,我有以下程序,其中有两种情况,一次使用参数
with_append = True
调用 run 函数,另一次使用 with_append = False
。
在第一种情况下,垃圾收集会介入,整个程序需要更长的时间才能完成。在第二种情况下,没有垃圾收集活动。您可以通过使用 py-spy
运行程序并使用 native
选项来看到这一点。
下面是示例程序和两种情况的输出。
import time
COUNT = 10000000
class User:
def __init__(self, name):
self.name = name
def run(with_append):
l = []
for i in range(COUNT):
u = User(f"user {i}")
if with_append:
l.append(u)
ts = time.time()
run(with_append=True)
print("function completed - ", time.time() - ts)
使用参数调用运行:with_append = True
输出:python demo.py
function completed - 10.940133094787598
输出:py-spy top --native -- python demo.py
Total Samples 1000
GIL: 100.00%, Active: 100.00%, Threads: 1
%Own %Total OwnTime TotalTime Function (filename)
53.00% 53.00% 4.51s 4.51s gc_collect_main (libpython3.10.so.1.0)
38.00% 100.00% 4.41s 10.00s run (demo3.py)
9.00% 10.00% 1.07s 3.13s __init__ (demo3.py)
0.00% 0.00% 0.010s 0.010s 0x7f91a5b2a746 (libc-2.31.so)
0.00% 100.00% 0.000s 10.00s <module> (demo3.py)
0.00% 53.00% 0.000s 4.51s gc_collect_with_callback (libpython3.10.so.1.0)
一半时间花在 gc_collect_main 上。
使用参数调用运行:with_append = False
输出:python demo.py
function completed - 4.351471424102783
输出:py-spy top --native -- python demo.py
Total Samples 400
GIL: 100.00%, Active: 100.00%, Threads: 1
%Own %Total OwnTime TotalTime Function (filename)
85.00% 100.00% 3.38s 4.00s run (demo3.py)
13.00% 14.00% 0.560s 0.600s __init__ (demo3.py)
1.00% 1.00% 0.020s 0.020s unicode_dealloc (libpython3.10.so.1.0)
1.00% 1.00% 0.020s 0.020s 0x7fe05fca5742 (libc-2.31.so)
0.00% 0.00% 0.010s 0.010s 0x7fe05fca5746 (libc-2.31.so)
0.00% 0.00% 0.010s 0.010s 0x7fe05fca5724 (libc-2.31.so)
0.00% 100.00% 0.000s 4.00s <module> (demo3.py)
为什么在这两种情况下都不使用引用计数来释放内存?为什么垃圾收集需要花费更多时间来删除对象?
每当您重新分配
u
时,旧 User
对象的引用计数就会下降到零,使其符合垃圾回收条件,因此在循环结束之前,它能够即时收集 u
包含的 User 对象。所以它等到函数结束时才对列表执行垃圾收集,而不是在执行期间执行,因为列表的引用计数在列表超出范围之前不会达到零。
很可能,在这两种情况下,对象都是通过引用计数删除的。
在场景1中,
gc_collect_main
在循环中间自动调用以检测具有循环引用的可收集对象。在场景1中,实际上并没有删除任何对象。但是,这种检测执行了数万次,因此总体上看起来很耗时。
这个自动检测是由对象数量增加触发的(这个我不是很确定),所以在场景2的循环中不执行,每次循环结束时通过引用计数删除对象.
您可以将以下代码放在脚本的顶部以检查执行检测的时间。
import gc
gc.set_debug(gc.DEBUG_STATS)
或者,在循环期间明确禁用检测应该会显着提高性能。
import gc
def run(with_append):
gc.disable()
l = []
for i in range(COUNT):
u = User(f"user {i}")
if with_append:
l.append(u)
gc.enable()
注意,官方文档中提到了这一点。
由于收集器补充了 Python 中已经使用的引用计数,如果您确定您的程序不会创建引用循环,则可以禁用收集器。
很明显,Python 无法自动判断是否存在循环引用(即是否需要垃圾回收),所以如果这是一个问题,您必须以这种方式显式禁用它。