【问题标题】:Can I "prime" the CLR GC to expect profligate memory use?我可以“启动”CLR GC 以期待大量使用内存吗?
【发布时间】:2013-12-11 11:33:27
【问题描述】:

我们有一个执行大量内存分配(短期和长期)的服务器应用。我们在启动后不久就看到了大量的 GC2 收集,但这些收集在一段时间后会平静下来(即使内存分配模式是恒定的)。 这些系列很早就达到了性能。

我猜这可能是由 GC 预算引起的(对于 Gen2?)。是否可以通过某种方式(直接或间接)设置此预算以使我的服务器在开始时表现更好?

我看到的一组违反直觉的结果:我们大幅减少了内存(和大对象堆)分配量,从长远来看性能有所提高,但早期性能变得更糟,并且“安定下来”的时间越来越长。

GC 显然需要一段时间才能意识到我们的应用程序是内存大户并相应地适应。这个事实我已经知道了,怎么说服GC呢?

编辑

  • 操作系统:64 位 Windows Server 2008 R2
  • 我们正在使用 .Net 4.0 ServerGC 批处理延迟。尝试了 4.5 和 3 种不同的延迟模式,虽然平均性能略有提高,但最坏情况下的性能实际上恶化了

编辑2

  • GC 峰值可能会花费一倍的时间(我们所说的秒数)从可接受变为不可接受
  • 几乎所有的峰值都与第 2 代集合相关
  • 我的测试运行导致最终的堆大小为 32GB。最初的泡沫会持续 1/5 的运行时间,之后的性能实际上更好(不那么频繁的峰值),即使堆在增长。接近测试结束时的最后一个尖峰(具有最大的堆大小)与初始“训练”期间的 2 个尖峰的高度相同(即一样差)(堆小得多)

【问题讨论】:

  • 你观察到的符合我的理解。当应用程序启动时,它正在经历一个恒定的增长期。每次应用程序达到(软)内存上限时,它都会运行一次 GC,以查看它是否可以在请求更多空间之前腾出空间。我还没有遇到任何让您在应用启动时保留大内存块的东西。
  • 您能告诉我您使用的是哪个操作系统吗?我的意思是 32 位还是 64 位?
  • 你不能,GC团队完全相信他们应该是配置收集器的人。而且您永远无法准确地提供可以调整算法以根据使用情况动态调整的配置。所以他们没有给我们选择,这就是它结束的地方。
  • 也许你可以分配几百兆字节的byte[1024],直到它们都被分配为止。然后你放下它们并开始正常启动。这可能会扩大 GC 预算。不过,我认为这是一个可怕的 hack。
  • 尝试一个完全不同的解决方案:在后台启动您的应用程序的第二个副本,对其进行预热,然后使用 IIS 绑定自动将其与当前实时版本交换。这样一来,服务消费者就不会产生任何启动成本。

标签: c# .net memory-management garbage-collection


【解决方案1】:

.NET 中超大堆的分配可能非常快,并且阻塞集合的数量不会阻止它这么快。您观察到的问题是由于您不只是分配,而且还有导致依赖关系重组和实际垃圾收集的代码,所有这些都是在分配进行的同时进行的。

有一些技巧需要考虑:

  • 尝试使用 LatencyMode (http://msdn.microsoft.com/en-us/library/system.runtime.gcsettings.latencymode(v=vs.110).aspx),在您主动加载数据时将其设置为 LowLatency - 也请参阅 cmets 到此答案

  • 使用多个线程

  • 在主动加载时不要填充对新分配对象的交叉引用;首先经过主动分配阶段,只使用整数索引来交叉引用项,而不是托管引用;然后强制完全 GC 几次以在 Gen2 中拥有所有内容,然后才填充您的高级数据结构;您可能需要重新考虑您的反序列化逻辑才能实现这一点

  • 尝试尽早将最大的根集合(对象数组、字符串)强制到第二代;在开始填充数据(加载数百万个小对象)之前,通过预分配它们并强制执行两次完全 GC 来做到这一点;如果您使用某种通用字典,请确保尽早预先分配其容量,以避免重组

  • 任何大的引用数组都是 GC 开销的重要来源 - 直到数组和引用的对象都在 Gen2 中;数组越大 - 开销越大;更喜欢索引数组而不是引用数组,尤其是对于临时处理需求

  • 避免在任何线程上处于活动加载阶段时释放或提升许多实用程序或临时对象,仔细查看代码中的字符串连接、装箱和“foreach”迭代器不会自动优化为“for”循环

  • 如果您有一个引用数组和具有一些长时间运行的紧密循环的函数调用层次结构,请避免引入从数组中某个位置缓存引用值的局部变量;相反,缓存偏移值并在所有级别的函数调用中继续使用类似“myArrayOfObjects [offset]”的构造;它在处理预填充的 Gen2 大型数据结构方面帮助了我很多,我个人的理论是,这有助于 GC 管理对本地线程数据结构的临时依赖,从而提高并发性

据我所知,在应用启动期间使用多个线程填充高达 ~100 Gb RAM 时,以下是这种行为的原因:

  • 当 GC 将数据从一代移动到另一代时,它实际上会复制它,从而修改所有引用;因此,您在主动负载阶段拥有的交叉引用越少 - 越好

  • GC 维护了很多管理引用的内部数据结构;如果您对引用本身进行大量修改 - 或者如果您在 GC 期间必须修改大量引用 - 在阻塞和并发 GC 期间会导致显着的 CPU 和内存带宽开销;有时我观察到 GC 在没有任何收集的情况下不断消耗 30-80% 的 CPU - 只需进行一些处理,这看起来很奇怪,直到您意识到任何时候您将某个数组或某个临时变量的引用放在紧密循环中时,GC必须修改并有时重新组织依赖跟踪数据结构

  • 服务器 GC 使用特定于线程的 Gen0 段,并且能够将整个段推送到下一代(无需实际复制数据 - 虽然不确定这一点),在设计多线程数据加载过程时请记住这一点

  • ConcurrentDictionary 虽然是一个很棒的 AP​​I,但在具有多核的极端情况下,当对象数量超过几百万时(考虑使用针对并发插入优化的非托管哈希表,例如英特尔的待定)

  • 如果可能或适用,请考虑使用本机池分配器(英特尔 TBB,再次)

顺便说一句,.NET 4.5 的最新更新支持大对象堆的碎片整理。升级到它的另一个重要理由。

.NET 4.6 还有一个 API 可以在满足某些条件时要求不进行任何 GC (GC.TryStartNoGCRegion):https://msdn.microsoft.com/en-us/library/dn906202(v=vs.110).aspx

另见 Maoni Stephens 的相关帖子:https://blogs.msdn.microsoft.com/maoni/2017/04/02/no-gcs-for-your-allocations/

【讨论】:

  • LowLatency 在文档中有此警告此设置仅适用于工作站垃圾收集。因此,如果您使用的是服务器 GC,则需要寻找其他地方。 SustainedLowLatency 没有相同的要求,但有不同的警告,仅在 4.5 中可用
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-02-21
  • 1970-01-01
  • 1970-01-01
  • 2010-12-06
  • 2011-02-01
  • 2017-06-29
  • 1970-01-01
相关资源
最近更新 更多