【问题标题】:How to avoid garbage collection in real time .NET application?如何避免实时 .NET 应用程序中的垃圾收集?
【发布时间】:2008-09-17 16:54:11
【问题描述】:

我正在编写一个金融 C# 应用程序,它接收来自网络的消息,根据消息类型将它们转换为不同的对象,最后在它们上应用应用程序业务逻辑。

关键是在应用了业务逻辑之后,我很确定我再也不需要这个实例了。我不想等待垃圾收集器释放它们,而是明确地“删除”它们。

在 C# 中是否有更好的方法来做到这一点,我应该使用对象池来始终重用同一组实例还是有更好的策略。

目标是避免垃圾收集在时间关键的过程中使用任何 CPU。

【问题讨论】:

  • 在 .Net 中,当 (a) 创建给定对象的速度很慢,或 (b) 在 32 位应用程序中,为了减少数组/列表/字典的内存碎片,池化很有用等使用大于 64 KB 的数据块,例如> 8K 元素 x 8 B/元素,或 16K 元素 x 4 B/元素。 .Net 从不重新定位(移动)如此大的块,如果您每次请求不同的大小,这可能会导致内存碎片。更安全地分配足够大的 # 元素并保留它们。 (但在不使用时清除它们的字段,以便 GC 可以回收它们指向的任何内容。)如果超出分配的大小,则将 # 个元素加倍。
  • 在极端情况下,我有一个 32 位应用程序(由于依赖项在 64 位中不起作用),所以即使我使用的是具有大量 RAM 的 64 位 Windows ,为了避免内存碎片,我必须保留列表列表,以便每个子列表适合 64 KB,这样 .Net 就不会在“大对象堆”上分配。因此它可以在 GC 期间“压缩”内存,而不是在应用程序的 4 GB 地址空间中创建大的“洞”。这个特定的应用程序是有问题的,因为它包含大量来自本机(非 .Net)内存的 DirectX 分配。非 .Net 和 .Net 分配的混合导致了碎片化。

标签: c# .net garbage-collection real-time finance


【解决方案1】:

不要立即删除它们。为每个对象调用垃圾收集器是个坏主意。通常你真的根本不想弄乱垃圾收集器,即使是时间关键的进程也只是等待发生的竞争条件,如果它们那么敏感的话。

但是,如果您知道您的应用会有繁忙和轻负载时段,您可以在达到轻负载时段时尝试更通用的 GC.Collect(),以鼓励在下一个繁忙时段之前进行清理。

【讨论】:

  • 这没问题,只要您了解 GC.Collect 不提供确定性垃圾收集。您只是告诉 GC 运行其循环,在这种情况下确定是否收集内存。对 GC.Collect 的调用并不能保证内存收集。
  • 请注意,过于频繁地调用 GC.Collect() 弊大于利:任何仍然具有引用的对象都将从第 0 代移动到第 1 代,或者从第 1 代到第 2 代。任何此类对象都会停留更长时间/需要更多工作才能让 GC 移除。只有在没有消息仍在处理时才调用 GC.Collect(),这样就不会引用任何临时数据。
【解决方案2】:

看这里:http://msdn.microsoft.com/en-us/library/bb384202.aspx

你可以告诉垃圾收集器你正在做一些关键的事情,它会尽力对你好。

【解决方案3】:

你自己打了 - 使用对象池并重用这些对象。对这些对象的调用语义需要隐藏在工厂外观后面。您需要以某种预定义的方式扩展池。每次达到极限时,大小可能会翻倍——高水位算法或固定百分比。我真的强烈建议你不要调用 GC.Collect()。

当池上的负载变得足够低时,您可以缩小池并最终触发垃圾回收——让 CLR 担心。

【讨论】:

    【解决方案4】:

    尝试对垃圾收集器进行事后判断通常是一个非常糟糕的主意。在 Windows 上,垃圾收集器是 a generational one 并且可以依靠它非常有效。这个一般规则有一些值得注意的例外 - 最常见的是发生一次性事件,您知道事实将导致许多旧对象死亡 - 一旦对象被提升到 Gen2(寿命最长)他们倾向于闲逛。

    在您提到的情况下,您听起来好像正在生成许多短期对象 - 这些将导致 Gen0 集合。无论如何,这些都相对经常发生,并且是最有效的。如果您愿意,您可以通过拥有可重用的对象池来避免它们,但最好在采取此类操作之前确定 GC 是否是性能问题 - CLR 分析器是执行此操作的工具。

    应该注意,垃圾收集器在不同的 .NET 框架上是不同的 - 在紧凑型框架(在 Xbox 360 和移动平台上运行)上,它是一个非分代 GC,因此您必须更多小心你的程序产生了什么垃圾。

    【讨论】:

      【解决方案5】:

      强制 GC.Collect() 通常是一个坏主意,让 GC 去做它最擅长的事情。听起来最好的解决方案是使用可以在必要时增长的对象池——我已经成功地使用了这种模式。

      这样您不仅可以避免垃圾收集,还可以避免常规分配成本。

      最后,您确定 GC 给您带来了问题吗?在实施任何节省性能的解决方案之前,您可能应该测量并证明这一点 - 您可能会给自己带来不必要的工作!

      【讨论】:

        【解决方案6】:

        “目标是避免垃圾收集在时间关键的过程中使用任何 CPU”

        问:如果时间紧迫,你的意思是你正在听一些深奥的硬件,你不能错过中断?

        答:如果是这样,那么 C# 不是要使用的语言,您需要 Assembler、C 或 C++。

        问:如果按时间关键,您的意思是当管道中有很多消息时,您不想让垃圾收集器减慢速度?

        答:如果是这样,您就不必担心了。听起来您的对象寿命很短,这意味着垃圾收集器将非常有效地回收它们,而不会出现任何明显的性能滞后。

        但是,唯一确定的方法是对其进行测试,将其设置为在夜间运行,处理源源不断的测试消息流,如果您的性能统计数据能够在 GC 启动时发现(甚至如果你能发现它,如果它真的很重要,我会更加惊讶)。

        【讨论】:

          【解决方案7】:

          对垃圾收集器的行为有一个很好的理解和感受,你就会明白为什么不推荐你在这里的想法。除非你真的喜欢 CLR 花时间重新排列内存中的对象很多。

          【讨论】:

            【解决方案8】:

            应用程序的密集程度如何?我编写了一个应用程序,它以 8KB 块捕获 3 个声卡(托管 DirectX,44.1KHz,立体声,16 位),并通过 TCP/IP 将 3 个流中的 2 个发送到另一台计算机。 UI 为 3 个通道中的每一个呈现音频电平表和(平滑)滚动标题/艺术家。这可以在 XP、1.8GHz、512MB 等的 PC 上运行。该应用程序使用大约 5% 的 CPU。

            我没有手动调用 GC 方法。但我确实不得不调整一些浪费的东西。我使用 RedGate 的 Ant profiler 来磨练浪费的部分。一个很棒的工具!

            我想使用预先分配的字节数组池,但托管的 DX 程序集在内部分配字节缓冲区,然后将其返回给应用程序。原来我没必要。

            【讨论】:

            • 完全非 astroturf 回复,但我正在阅读这个问题,因为我们面临与原始海报类似的问题,其中提到的第一件事是 Ants。
            • 用于 44.1kHz 立体声 16 位采样流的 8kB 缓冲区可产生大约 45ms 的延迟。这远非实时。除非你坚持使用非分配原生编程,否则低于 5ms 会更麻烦。
            【解决方案9】:

            如果绝对时间紧迫,那么您应该使用 C/C++ 等确定性平台。即使调用 GC.Collect() 也会产生 CPU 周期。

            您的问题始于您希望节省内存但摆脱对象的建议。这是一个空间关键优化。你需要决定你真正想要什么,因为 GC 比人类更擅长优化这种情况。

            【讨论】:

              【解决方案10】:

              从它的声音来看,您似乎在谈论确定性终结(C++ 中的析构函数),这在 C# 中不存在。您会在 C# 中找到最接近的东西是 Disposable 模式。基本上你实现了 IDisposable 接口。

              基本模式是这样的:

              public class MyClass: IDisposable
              {
                  private bool _disposed;
              
                  public void Dispose()
                  {
                      Dispose( true );
                      GC.SuppressFinalize( this );
                  }
              
                  protected virtual void Dispose( bool disposing )
                  {
                      if( _disposed )    
                          return;
              
                      if( disposing )
                      {
                          // Dispose managed resources here
                      }
              
                      _disposed = true;
                  }
              }
              

              【讨论】:

              • 如果您的类有其他引用类型作为属性,这是否危险?它们是否仍会留在垃圾收集或处理后立即处理?
              • @jocull Dispose 实际上与垃圾收集无关。您可以做的最好的事情是确定性地处置 un 托管资源(当然,在这种情况下您还应该有一个终结器),或者设置一些对 null 的内部引用(从而使它们可能符合条件供收藏)。唯一具有确定性的部分是非托管资源。托管内存是始终托管的 - 例如,这可以防止您意外删除仍然具有引用的内容。甚至堆栈也不保证在那里。这是一个实现细节,不是规范的一部分。
              【解决方案11】:

              您可以在池中拥有有限数量的每种类型的实例,并重用已完成的实例。池的大小取决于您要处理的消息量。

              【讨论】:

                【解决方案12】:

                不是每次收到消息时都创建一个新的对象实例,为什么不重用已经使用过的对象呢?这样您就不会与垃圾收集器作斗争,而且您的堆内存也不会碎片化。**

                对于每种消息类型,您可以创建一个池来保存未使用的实例。每当您收到网络消息时,您都会查看消息类型,从适当的池中拉出等待实例并应用您的业务逻辑。之后,您将该消息对象的实例放回它的池中。

                您很可能希望使用实例“延迟加载”您的池,以便您的代码轻松扩展。因此,您的池类将需要检测空实例何时被拉出并在分发之前将其填满。然后,当调用代码将其放回池中时,它就是一个真实的实例。

                ** “对象池是一种使用模式,它允许对象被重用而不是分配和释放,这有助于防止堆碎片以及代价高昂的 GC 压缩。”

                http://geekswithblogs.net/robp/archive/2008/08/07/speedy-c-part-2-optimizing-memory-allocations---pooling-and.aspx

                【讨论】:

                  【解决方案13】:

                  理论上,如果您的 CPU 负载过重或除非确实需要,GC 不应该运行。但是,如果必须,您可能只想将所有对象保存在内存中,也许是一个单例实例,除非您准备好,否则永远不要清理它们。这可能是保证 GC 何时运行的唯一方法。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2010-11-12
                    • 2018-03-15
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多