【问题标题】:Does breaking a linked list help Python's garbage collector?打破链表对 Python 的垃圾收集器有帮助吗?
【发布时间】:2021-08-09 11:37:25
【问题描述】:

我在想,当我有机会时,我是否应该在不再需要 Python 中分解一个单连接链表,或者我不应该为它烦恼。

列表示例:

class Link:
    def __init__(self, next=None):
        self.next = next


L = Link(Link(Link(Link())))

断开链接:

def break_(link):
    if link is not None:
        break_(link.next)
        link.next = None  # link broken

# application
break_(L)

树示例:

class Node:
    def __init__(self, l=None, r=None):
        self.l = l
        self.r = r

T = \
Node(
    Node(
        Node(), 
        Node()
    ),
    Node(
        Node(), 
        Node()
    ),
)

断开链接:

def break_(node):
    if node is not None:
        break_(node.l)
        break_(node.r)
        node.l = None  # link broken
        node.r = None  # link broken
        
# application
break_(T)

基本上,我想知道的是,在这种情况下编写代码的最佳性能方式是什么。 GC 准备好处理大型链接结构了吗?它是否不必使用计数器等运行可能很长的 DFS 来确定可以释放哪些对象?仅仅断开链接并给 GC 一堆零引用的松散对象不是更简单吗?

我可以对此进行基准测试,但我正在寻找解释(最好来自 Karl),如果有的话。谢谢。

【问题讨论】:

  • 谁是 Karl,您的应用/解决方案是否需要对 GC 进行额外控制?
  • 我最好不要控制 GC。我只是想知道在 Python 中摆脱链表的更快方法是什么以及为什么。
  • 如果您不需要控制 GC,按原样为您的用例编写程序就足够了。当不再需要链表时,GC 会删除链表。
  • 我想你关心 CPython 吗?如果是这样,请添加 cpython 标签。 Python 本身是一种语言规范,它不会对 GC 细节强加任何东西。
  • 其次,“断开链接”是什么意思?您能否提供一个代码示例来说明如何执行此操作?为什么您认为这比幕后发生的工作要少?

标签: python linked-list garbage-collection


【解决方案1】:

您可以在您的类中添加__del__ 方法以查看对象何时将被清理:

class Node:
    def __init__(self, name, l=None, r=None):
        self.name = name
        self.l = l
        self.r = r

    def __del__(self):
        print(f'__del__ {self.name}')


T = \
Node('0',
    Node('0L',
        Node('0LL'), 
        Node('0LR'),
    ),
    Node('0R',
        Node('0RL'), 
        Node('0RR'),
    ),
)
del T  # decreases the reference count of T by 1

del T 发生的情况是 T 引用的对象的引用计数减 1。在这种情况下,引用计数恰好达到 0。CPython 知道这一点,因为它存储了一个引用计数与每个对象一起。当对象的引用计数达到 0 时,该对象将被标记为清理。此清理发生在将控制权交还给用户代码之前。所以对于上面的例子,这意味着T最初引用的对象立即被清理了。这首先调用__del__,然后调用相应的 C 代码进行清理。这反过来通过对象的实例字典并将存储在那里的每个其他对象的引用计数减少 1。如果另一个对象的引用计数在该过程中达到 0,它也被标记为清理。然后重复这个过程,直到没有对象被标记为清理并且控制权被交还给主循环。

这是示例的输出:

__del__ 0
__del__ 0L
__del__ 0LL
__del__ 0LR
__del__ 0R
__del__ 0RL
__del__ 0RR

如上所述,当 0 被清理时,它引用的所有其他对象的引用计数都减 1(即 0L0R),并且如果引用计数达到 0,也会被清理。

如果您通过将相应的实例属性设置为None 来手动断开列表的链接,这会增加额外的开销,因为它必须实际修改内存中的所有这些对象(更新它们的实例字典)。子对象的引用计数达到 0 在这里更多的是副产品。另请注意,由于clean 函数首先进入深度,因此清理顺序发生了变化:

def clean(node):
    if node is not None:
        clean(node.l)
        clean(node.r)
        node.l = None  # this updates the object in memory
        node.r = None

clean(T)

生产

__del__ 0LL
__del__ 0LR
__del__ 0RL
__del__ 0RR
__del__ 0L
__del__ 0R
__del__ 0

【讨论】:

  • 感谢您的回答。在我的机器上的发现是这样的:如果你自己实现__del__,你会减慢 GC 而没有清理两次。如果没有__del__,在调用del 之前进行清理会使 GC 运行速度提高 3 倍(对于这个微型树),但清理本身比没有清理的独立 GC 花费的时间多 7 倍。所以你对开销绝对是正确的,但是关于“只是一个副产品”的部分是不正确的,因为加速是不可忽略的。不管怎样,你帮了大忙。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-30
  • 2019-11-08
  • 1970-01-01
相关资源
最近更新 更多