GC急切根集合

5

Pro.NET性能-优化您的C#应用的第96页中,讨论了GC eager root收集:

对于每个本地变量,JIT将嵌入一个表,其中包含变量作为根仍然相关的最早和最晚指针地址。 GC在执行堆栈遍历时使用这些表。

然后提供了以下示例:

    static void Main(string[] args)
    {
        Widget a = new Widget();
        a.Use();
        //...additional code
        Widget b = new Widget();
        b.Use();
        //...additional code
        Foo(); //static method
    }

然后它说:
引述上面的讨论,将代码分成更小的方法并使用较少的本地变量不仅是一个好的设计措施或软件工程技术。对于.NET GC而言,这还可以提供性能优势,因为您有更少的本地根!这意味着编译方法时JIT需要处理的内容更少,占用根IP表格的空间更少,GC执行堆栈跟踪时需要处理的内容也更少。
我不明白如何通过将代码分成更小的方法来帮助。我已将代码分成以下部分:
    static void Main(string[] args)
    {
        UseWidgetA();
        //...additional code
        UseWidgetB();
        //...additional code
        Foo(); //static method
    }

    static void UseWidgetA()
    {
        Widget a = new Widget();
        a.Use();
    }

    static void UseWidgetB()
    {
        Widget b = new Widget();
        b.Use();
    }
}

本地跟踪器减少:

为什么本地跟踪器会减少呢? 每种方法中还是有一个本地跟踪器。

JIT编译时负担减轻:

这难道不会让情况变得更糟吗?因为需要2个额外的表来存储2个额外的方法。JIT编译器仍然需要记录变量在每个方法中相关的最早和最晚指针,在这方面它只是要处理更多的方法。

GC执行堆栈遍历时负担减轻:

有更多较小的方法如何能使GC在执行堆栈遍历时负担减轻呢?


这是相当深奥的优化。通常,小方法并不是坏事,然而,你之所以可以这样分割示例代码,是因为在后续步骤中没有使用变量,所以你可以将它们的范围限制在较小的方法中。你也可以通过在一个方法内使用花括号来实现类似的效果,但一般情况下,JIT足够聪明,能够识别未使用的变量,并将其从GC根中省略掉。参见昨天的这篇问答,正好讨论了这个问题。因此,仅考虑GC性能而言,你不需要分割方法。 - Holger
1个回答

5
我不知道Sasha的想法,但我可以提供我的个人观点。
首先,我把它看作是一个普适规则 - 当你将一个方法分割成更小的方法时,由于某些子程序只在有条件时执行,因此有可能有一些部分不需要被JIT编译。
其次,JIT确实会生成所谓的GC info以记录活动栈。方法越大,GC info就越大,理论上,在GC期间对其进行解释也会带来更大的开销。不过,这可以通过将GC Info拆分为多个部分来解决。然而,关于栈的活动性的信息仅存储在所谓的safe points中。存在两种类型的方法:
  • partially interruptible - 仅在调用其他方法时才存在安全点。这使得方法较少"可挂起",因为运行时需要等待这样的安全点来挂起一个方法,但对于GC info来说却会消耗较少的内存。
  • fully interruptible - 方法的每个指令都被视为安全点,这显然使得方法非常"可挂起",但需要占用大量存储空间(与代码本身相似)。
  • 正如《运行时之书》所说:“JIT根据启发式方法选择是生成全面的还是部分中断的代码,以达到代码质量、GC info大小和GC暂停延迟之间的最佳平衡”。 在我看来,更小的方法有助于JIT做出更好的决策(基于其启发式方法),将方法部分或完全中断。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接