垃圾收集和并行.查看VS2015升级后的每个问题

本文关键字:问题 VS2015 查看 并行 | 更新日期: 2023-09-27 18:05:41

我有一些代码在我自己的类r c# DataFrame类中处理数百万行数据。有很多平行的。ForEach调用并行遍历数据行。这段代码已经在VS2013和。net 4.5上运行了一年多,没有任何问题。

我有两台开发机器(A和B),最近升级了机器A到VS2015。我开始注意到我的代码中有一半的时间出现了奇怪的间歇性冻结。让它运行很长一段时间,结果是代码最终完成了。只需15-120分钟,而不是1-2分钟。

尝试打破所有使用VS2015调试器不断失败的原因。我插入了一些log语句。事实证明,当并行期间存在Gen2收集时,就会发生这种冻结。ForEach循环(比较每个Parallel前后的集合计数)。ForEach循环)。整个额外的13-118分钟都是在平行轨道上度过的。ForEach循环调用恰好与Gen2集合重叠(如果有的话)。如果在任何并行期间没有Gen2收集。ForEach循环(大约50%的时间当我运行它时),然后一切在1-2分钟内完成。

当我在机器A上的VS2013中运行相同的代码时,我得到相同的冻结。当我在机器B(从未升级)上运行VS2013中的代码时,它可以完美地工作。它在一夜之间运行了几十次,没有结冰。

我注意到/尝试过的一些事情:

  • 冻结发生有或没有在机器A上附加调试器(我认为这是与VS2015调试器在第一)
  • 无论我是在调试模式还是发布模式下构建,都会发生冻结
  • 如果我的目标是。net 4.5或。net 4.6,就会发生冻结
  • 我尝试禁用RyuJIT,但这并不影响冻结

我根本没有改变默认的GC设置。根据GCSettings,所有运行都是在LatencyMode Interactive和IsServerGC为false的情况下进行的。

我可以在每次调用并行之前切换到低延迟。每个都可以,但我真的更想知道发生了什么。

还有人在平行空间看到奇怪的冻结吗?在VS2015升级后的每一个?你知道下一步该怎么做吗?

更新1:为上面模糊的解释添加一些示例代码…

下面是一些示例代码,我希望能够演示这个问题。这段代码在B机器上持续运行10-12秒。它遇到了许多Gen2集合,但它们几乎不需要花费任何时间。如果我取消注释这两条GC设置行,我可以强制它没有Gen2收集。比30-50秒慢一些。

现在在我的A机器上,代码花费的时间是随机的。大概在5到30分钟之间。而且,它遇到的第2代收藏品越多,情况似乎就越糟。如果我取消注释这两个GC设置行,在机器A上也需要30-50秒(与机器B相同)。

这可能需要在行数和数组大小方面进行一些调整,以便在另一台机器上显示。

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using System.Runtime;    
public class MyDataRow
{
    public int Id { get; set; }
    public double Value { get; set; }
    public double DerivedValuesSum { get; set; }
    public double[] DerivedValues { get; set; }
}
class Program
{
    static void Example()
    {
        const int numRows = 2000000;
        const int tempArraySize = 250;
        var r = new Random();
        var dataFrame = new List<MyDataRow>(numRows);
        for (int i = 0; i < numRows; i++) dataFrame.Add(new MyDataRow { Id = i, Value = r.NextDouble() });
        Stopwatch stw = Stopwatch.StartNew();
        int gcs0Initial = GC.CollectionCount(0);
        int gcs1Initial = GC.CollectionCount(1);
        int gcs2Initial = GC.CollectionCount(2);
        //GCSettings.LatencyMode = GCLatencyMode.LowLatency;
        Parallel.ForEach(dataFrame, dr =>
        {
            double[] tempArray = new double[tempArraySize];
            for (int j = 0; j < tempArraySize; j++) tempArray[j] = Math.Pow(dr.Value, j);
            dr.DerivedValuesSum = tempArray.Sum();
            dr.DerivedValues = tempArray.ToArray();
        });
        int gcs0Final = GC.CollectionCount(0);
        int gcs1Final = GC.CollectionCount(1);
        int gcs2Final = GC.CollectionCount(2);
        stw.Stop();
        //GCSettings.LatencyMode = GCLatencyMode.Interactive;
        Console.Out.WriteLine("ElapsedTime = {0} Seconds ({1} Minutes)", stw.Elapsed.TotalSeconds, stw.Elapsed.TotalMinutes);
        Console.Out.WriteLine("Gcs0 = {0} = {1} - {2}", gcs0Final - gcs0Initial, gcs0Final, gcs0Initial);
        Console.Out.WriteLine("Gcs1 = {0} = {1} - {2}", gcs1Final - gcs1Initial, gcs1Final, gcs1Initial);
        Console.Out.WriteLine("Gcs2 = {0} = {1} - {2}", gcs2Final - gcs2Initial, gcs2Final, gcs2Initial);
        Console.Out.WriteLine("Press Any Key To Exit...");
        Console.In.ReadLine();
    }
    static void Main(string[] args)
    {
        Example();
    }
}

更新2:只是为了把东西从评论中移出来,以便将来的读者…

这个hotfix: https://support.microsoft.com/en-us/kb/3088957完全修复了这个问题。在申请之后,我没有看到任何缓慢的问题。

结果与Parallel没有任何关系。我相信基于此:http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx虽然修复确实提到并行。

垃圾收集和并行.查看VS2015升级后的每个问题

这确实执行得非常差,后台GC在这里对您不利。我注意到的第一件事是Parallel.ForEach()使用了太多的任务。线程池管理器错误地将线程行为解释为"因I/o而陷入困境"。并启动额外的线程。这使问题变得更糟。解决方法是:

var options = new ParallelOptions();
options.MaxDegreeOfParallelism = Environment.ProcessorCount;
Parallel.ForEach(dataFrame, options, dr => {
    // etc..
}

这可以更好地了解VS2015中新的诊断中心的程序问题。它不需要很长时间,只有一个核心做任何工作,很容易从CPU的使用情况。偶尔会有尖峰,但持续时间不长,与橙色的GC标记一致。当您仔细查看GC标记时,您会发现它是一个gen #1收集。非常很长时间,在我的机器上大约6秒。

第1代收集当然不会花费那么长时间,您在这里看到的是第1代收集等待后台GC完成其工作。换句话说,实际上后台GC花费了6秒。只有当第0代和第1代段的空间足够大,不需要在后台GC处理时进行第2代收集时,后台GC才能有效。这款应用的工作方式不一样,它消耗内存的速度非常快。您看到的小峰值是多个任务被解除阻塞,能够再次分配数组。当第1代收集必须再次等待后台GC时,会迅速停止。

值得注意的是,这段代码的分配模式对GC非常不友好。它将长寿命数组(dr.DerivedValues)与短寿命数组(tempArray)交织在一起。给GC大量的工作,当它压缩堆,每个单独分配的数组将最终被移动。

. net 4.6 GC中明显的缺陷是后台收集似乎从来没有有效地压缩堆。它看起来就像它一遍又一遍地做这项工作,就好像以前的集合根本没有压缩一样。很难说这是设计使坏还是bug使坏,我已经没有一台干净的4.5版机器了。我当然倾向于bug。你应该在connect.microsoft.com上报告这个问题,让微软来检查一下。


一个解决方法是很容易得到的,你所要做的就是防止长寿命对象和短寿命对象的尴尬交错。你可以通过预先分配它们来完成:

    for (int i = 0; i < numRows; i++) dataFrame.Add(new MyDataRow { 
        Id = i, Value = r.NextDouble(), 
        DerivedValues = new double[tempArraySize] });
    ...
    Parallel.ForEach(dataFrame, options, dr => {
        var array = dr.DerivedValues;
        for (int j = 0; j < array.Length; j++) array[j] = Math.Pow(dr.Value, j);
        dr.DerivedValuesSum = array.Sum();
    });

当然,通过完全禁用后台GC。


更新:这篇博文中确认了GC错误。修复即将到来。


更新:一个修复程序发布。


更新:在。net 4.6.1中修复

我们(和其他用户)遇到了类似的问题。我们通过在应用程序的app.config中禁用后台GC来解决这个问题。

应用程序。配置gcConcurrent(非并发工作站GC)

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" />
    </startup>
<runtime>
    <gcConcurrent enabled="false" />
</runtime>

您也可以切换到服务器GC,尽管这种方法似乎使用更多内存(在不饱和机器上?)。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" />
    </startup>
<runtime>
    <gcServer enabled="true" />
</runtime>
</configuration>

看起来问题已经解决了,参见http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx