【问题标题】:Scalability of the .NET 4 garbage collector.NET 4 垃圾收集器的可扩展性
【发布时间】:2010-08-15 22:58:53
【问题描述】:

我最近对 ​​.NET 4 垃圾收集器进行了基准测试,从多个线程集中分配。当分配的值被记录在一个数组中时,我观察到没有像我预期的那样可伸缩性(因为系统争用对共享老年代的同步访问)。但是,当分配的值立即被丢弃时,我惊恐地发现当时也没有可伸缩性!

我曾预计临时案例几乎可以线性扩展,因为每个线程都应该简单地将nursery gen0 清理干净并重新开始,而不会争夺任何共享资源(没有任何东西可以保留给老一代,也没有 L2 缓存未命中,因为 gen0 很容易适合 L1 缓存)。

例如this MSDN article says:

无同步分配在多处理器系统上,托管堆的第 0 代被分成多个内存区域,每个线程使用一个区域。这允许多个线程同时进行分配,因此不需要对堆的独占访问。

谁能验证我的发现和/或解释我的预测和观察之间的差异?

【问题讨论】:

  • 定义“无可扩展性”的含义。
  • 您最好发布您的确切方法、测量的内容、测量方式和测量值。
  • 我在这里猜测,但我可能 Jon Harrop 正在 N 核计算机上运行他的测试,并使用 n = 1 到 N 个线程进行基准测试。缩放就是基准速度如何随 n 变化。
  • @Martin:没错。随着分配线程数的增加,总分配率是恒定的。
  • 你确定每个线程都在自己的核心上吗?

标签: c# f# garbage-collection multicore


【解决方案1】:

不太确定这是关于什么以及究竟你在你的机器上看到了什么。但是,您的机器上有两个不同版本的 CLR。 Mscorwks.dll 和 mscorsvc.dll。前者是您在工作站上运行程序时得到的,后者是在 Windows 的服务器版本之一(如 Windows 2003 或 2008)上运行的。

工作站版本对您的本地 PC 很友好,它不会占用所有机器资源。在 GC 进行期间,您仍然可以阅读您的电子邮件。服务器版本经过优化,可在服务器级硬件上扩展。大量 RAM(GC 不会那么快启动)和大量 CPU 内核(垃圾被收集在多个内核上)。您引用的文章可能涉及服务器版本。

您可以选择工作站上的服务器版本,使用 .config 文件中的 <gcServer> 元素。

【讨论】:

    【解决方案2】:

    不是对问题的完整答案,只是为了澄清一些误解:.NET GC 仅在工作站模式下并发。在服务器模式下,它使用 stop-the-world 并行 GC。更多详情here。 .NET 中的独立托儿所主要是为了避免分配同步;然而,它们是全局堆的一部分,不能单独收集。

    【讨论】:

    • “它们仍然是全局堆的一部分,不能单独收集”。这正是我需要知道的。谢谢!
    【解决方案3】:

    我可以大胆猜测发生了什么。

    (1) 如果你有一个线程并且在第 0 代有 M 个空闲空间,那么 GC 只会在 M 个字节分配后运行。

    (2) 如果你有 N 个线程并且 GC 将第 0 代划分为每个线程 N/M 个空间,则每次线程分配 N/M 个字节时 GC 将最终运行。这里最引人注目的是 GC 需要“停止世界”(即暂停所有正在运行的线程)以便标记来自线程根集的引用。这并不便宜。因此,GC 不仅会更频繁地运行,而且会在每个集合上做更多的工作。

    当然,另一个问题是多线程应用程序通常对缓存不太友好,这也会严重影响您的性能。

    我认为这不是 .NET GC 问题,而是一般的 GC 问题。一位同事曾经运行一个简单的“乒乓”基准测试,使用 SOAP 在两个线程之间发送简单的整数消息。当两个线程在不同的进程中时,基准测试的运行速度是原来的两倍,因为内存分配和管理是完全解耦的!

    【讨论】:

    • @Rafe:“GC 需要阻止这个世界”。你确定吗?我可以想象在 Nursery 生成中所有对象的根都在全局变量中的设计,(一个)线程本地堆栈和由写屏障生成的记忆集。
    • @Jon:有点晚了,所以我可能在这里偏离了基础,但不要求每个机器寄存器都由全局或堆栈插槽支持,而是否定很多代码生成优化?此外,写屏障并不便宜。我想到的是:GC 无法知道线程 i 没有将对本地对象的引用传递给线程 j,因此它需要检查 j 的根以查找对 i 的托儿所的引用。无论哪种方式,我对“多线程应用程序的性能”下的链接文章的阅读是 .NET GC 是一种停止世界的类型。
    • @Rafe:“不会要求每个机器寄存器都由全局或堆栈槽支持,而是否定大量代码生成优化”。不,我在 HLVM 中使用了该技术,它生成的代码非常快。
    • @Rafe: "GC 无法知道线程 i 没有将本地对象的引用传递给线程 j,因此它需要检查 j 的根以查找对 i 的托儿所的引用”。许多 GC 设计阻止共享数据的指针返回到线程本地托儿所。这完全取决于他们的设计。例如,这通常是通过在将指向它的指针写入全局堆时收集 Nursery 代来实现的。
    • @Jon: 当然,GC 只需要停止世界就可以在程序中的所有线程中找到根,但是停止世界和跟踪所有根集都相对昂贵考虑实际发生的事情。我想我们可能在这里谈论不同的目的。在 VM 上实现的语言空间内,.NET 非常好。然而,这些语言与“更精简”的语言(例如 C、C++、Mercury、OCaml)相比往往较差。我认为这部分是因为 VM 设计中的权衡以可移植性和灵活性的名义牺牲了一些性能。
    【解决方案4】:

    非常快速、易于查看(直接在根目录下,分配空值)和大量发布可以诱使 GC 变得急切,缓存本地堆的整个想法是一个美好的梦想 :-) 即使您有完全分离的线程-local heaps(你没有)句柄指针表仍然必须是完全易变的,只是为了使一般多 CPU 场景安全。哦,请记住,有很多线程,CPU 缓存是共享的,内核需要优先,所以它不只适合你:-)

    还要注意,带有双指针的“堆”有 2 个部分 - 要提供的内存块和句柄指针表(这样可以移动块,但您的代码始终只有一个地址)。这样的表是一个关键但非常精益的流程级资源,而强调它的唯一方法就是用大量的快速发布来淹没它——所以你设法做到了:-))

    一般来说 GC 的规则是 - 泄漏 :-) 当然不是永远,但只要你可以。如果您还记得人们如何到处说“不要强制 GC 收集”吗?这就是故事的一部分。此外,“stop the world”集合实际上比“concurrent”更有效,并且过去以循环窃取或调度程序合作的更好名称而闻名。只有标记阶段需要冻结调度程序,并且在服务器上会有多个线程在执行此操作(N 个内核无论如何都处于空闲状态:-) 另一个原因是它可以使播放视频等实时操作变得抖动,就像更长的线程量子一样。

    同样,如果您在短期和频繁的 CPU 突发(小型分配、几乎没有工作、快速释放)上与基础设施竞争,您将看到/测量的唯一内容将是 GC 和 JIT 噪音。

    如果这是真实的,即不仅仅是试验,你能做的最好的就是在堆栈(结构)上使用大值数组。它们不能被强制放到堆上,并且尽可能本地化,并且不受任何后门移动=>缓存必须爱它们:-)这可能意味着使用普通指针切换到“不安全”模式,也许自己做一些分配(如果你需要一些简单的东西,比如列表),但这是为踢出 GC 付出的小代价:-) 尝试强制数据进入缓存还取决于保持你的堆栈保持精简 - 记住你并不孤单。还给你的线程一些至少值得几个量子的工作 berween 版本可能会有所帮助。最坏的情况是,如果你在一个信号量内分配和释放。

    【讨论】:

      【解决方案5】:

      或者解释一下我的预测和观察之间的差异?

      基准测试很难。
      对不受您完全控制的子系统进行基准测试更加困难。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-04-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-04
        • 1970-01-01
        相关资源
        最近更新 更多