为什么没有循环引用的 Python 对象仍然会被垃圾收集删除?

问题描述 投票:0回答:2

如果一个函数在循环中创建了很多对象,这些对象在其他地方没有被引用,由于 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)

为什么在这两种情况下都不使用引用计数来释放内存?为什么垃圾收集需要花费更多时间来删除对象?

python performance garbage-collection reference-counting
2个回答
0
投票

每当您重新分配

u
时,旧
User
对象的引用计数就会下降到零,使其符合垃圾回收条件,因此在循环结束之前,它能够即时收集
u
包含的 User 对象。所以它等到函数结束时才对列表执行垃圾收集,而不是在执行期间执行,因为列表的引用计数在列表超出范围之前不会达到零。


0
投票

很可能,在这两种情况下,对象都是通过引用计数删除的。

在场景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 无法自动判断是否存在循环引用(即是否需要垃圾回收),所以如果这是一个问题,您必须以这种方式显式禁用它。

© www.soinside.com 2019 - 2024. All rights reserved.