我可以帮你翻译成中文。题目是:“我能够“预加载”CLR垃圾回收器以期望浪费的内存使用吗?”

8
我们有一个服务器应用程序,进行了许多内存分配(短期和长期)。我们在启动后不久看到了大量的GC2收集,但这些收集会在一段时间后平静下来(即使内存分配模式是恒定的)。
这些收集会影响性能。我猜测这可能是由于GC预算(针对Gen2?)。是否有办法设置此预算(直接或间接),以使我的服务器在开始时表现更佳?
我看到了一组反直觉的结果:我们大幅减少了内存(和大对象堆)分配量,长期性能得到了改善,但早期性能变差,并且“稳定下来”的时间更长。
GC显然需要一定的时间才能意识到我们的应用程序是一个内存占用量很大的程序,并相应地进行调整。我已经知道这个事实,如何说服GC呢?
编辑:
操作系统:64位Windows Server 2008 R2
我们使用.Net 4.0 ServerGC Batch Latency。尝试了4.5和3种不同的延迟模式,虽然平均性能略有提高,但最差情况的性能实际上恶化了。
编辑2:
GC峰值可以将时间(我们说秒)从可接受到不可接受的时间翻倍
几乎所有峰值都与Gen 2收集相关
我的测试运行导致最终的32GB堆大小。初始的泡沫期持续了前1/5的运行时间,在那之后的性能实际上更好(峰值不太频繁),即使堆正在增长。测试结束时的最后一个峰值(具有最大的堆大小)与初始“训练”期间的2个峰值(堆要小得多)相同高度(即一样糟糕)。

你观察到的情况与我的理解相符。当应用程序启动时,它将经历一个不断增长的阶段。每当应用程序达到(软)内存上限时,它都会进行GC运行,以查看是否可以在请求更多内存之前腾出空间。我还没有遇到任何可以在应用程序启动时预留大内存块的东西。 - Adam Houldsworth
2
你无法这样做,GC团队坚信他们应该配置收集器。而且你永远无法准确地提供适应算法动态调整的配置。所以他们没有给我们选择的机会,这就是结束的地方。 - Hans Passant
也许你可以分配几百兆的 byte[1024] 数组,一直保持它们被分配,直到所有它们都被分配。然后你将它们释放并开始正常启动。这可能会扩大垃圾回收的预算。但我认为这是可怕的 hack。 - usr
@usr:这是一个丑陋的黑客技巧,我们已经在考虑中了。如果有人已经做过这个,并且得到了良好的结果,并建议配置参数,我会感觉不那么肮脏。 - Rob
@HansPassant 那听起来像是一个答案。为什么不把它添加为一个答案呢? - Aleksandr Dubinsky
显示剩余6条评论
1个回答

6
在.NET中分配极大的堆可以非常快速,而阻塞集合的数量并不会影响其速度。您观察到的问题是由于您不仅进行了分配,还有代码导致依赖关系重组和实际垃圾回收,同时进行分配。有几种技术需要考虑:
  • 尝试使用LatencyMode(http://msdn.microsoft.com/en-us/library/system.runtime.gcsettings.latencymode(v=vs.110).aspx),在加载数据时将其设置为LowLatency - 参见此答案的评论

  • 使用多个线程

  • 在主动加载期间不要填充新分配对象的交叉引用; 首先通过活动分配阶段,仅使用整数索引交叉引用项,而不是托管引用; 然后强制进行几次完整GC,使所有内容都在Gen2中,然后再填充高级数据结构; 您可能需要重新考虑反序列化逻辑以实现此目的

  • 尝试尽早将最大的根集合(对象数组,字符串)强制转换为第二代; 在开始填充数据(加载数百万个小对象)之前,预先分配它们并强制进行两次完整GC; 如果您正在使用某种泛型Dictionary,请确保尽早预分配其容量,以避免重组

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

  • 在任何线程的活动加载阶段避免许多实用程序或临时对象被释放或升级,仔细查看您的代码以获取不能自动优化为'for'循环的字符串连接,装箱和“foreach”迭代器

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

以下是我从多线程应用程序启动时填充近100 GB RAM的行为中所学到的原因:
  • 当GC将数据从一个代移动到另一个代时,实际上是复制它并修改所有引用;因此,在活动加载阶段交叉引用越少,越好。

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

  • 服务器GC使用线程特定的Gen0段,并能够将整个段推送到下一个Gen(实际上不复制数据 - 对此我不确定),在设计多线程数据加载过程时请记住这一点。

  • ConcurrentDictionary虽然是一个很好的API,但在极端场景下具有多个核心且对象数超过几百万时,其性能不会很好(考虑使用优化用于并发插入的非托管哈希表,例如随Intel TBB提供的哈希表)。

  • 如果可能或适用,请考虑使用本机池分配器(再次使用Intel 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/


1
LowLatency在文档中有一个警告:“此设置仅适用于工作站垃圾回收”。因此,如果您使用的是服务器GC,则需要寻找其他解决方案。SustainedLowLatency没有相同的要求,但它有一个不同的警告,并且仅在4.5中可用。 - ta.speot.is

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