VS2015升级后的垃圾收集和Parallel.ForEach问题

时间:2015-07-31 13:57:02

标签: c# garbage-collection visual-studio-2015 parallel.foreach .net-4.6

我有一些代码可以在我自己的R-like C#DataFrame类中处理数百万个数据行。有许多Parallel.ForEach调用并行迭代数据行。这段代码使用VS2013和.NET 4.5运行了一年多而没有问题。

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

尝试使用VS2015调试器中断所有因某些原因而失败。所以我插入了一堆日志语句。事实证明,在Parallel.ForEach循环期间存在Gen2集合时会发生此冻结(比较每个Parallel.ForEach循环之前和之后的集合计数)。整个额外的13-118分钟用于任何Parallel.ForEach循环调用恰好与Gen2集合(如果有的话)重叠。如果在任何Parallel.ForEach循环期间没有Gen2集合(大约50%的时间运行它),那么一切都会在1-2分钟内完成。

当我在机器A上的VS2013中运行相同的代码时,我得到了相同的冻结。当我在机器B(从未升级过)的VS2013中运行代码时,它运行得很好。它一夜之间没有冻结了几十次。

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

  • 无论是否在机器A上附加调试器,都会发生冻结(我认为它最初是VS2015调试器的东西)
  • 无论我是在构建还是发布模式下,都会发生冻结
  • 如果我的目标是.NET 4.5或.NET 4.6
  • ,就会发生冻结
  • 我尝试禁用RyuJIT,但这并没有影响冻结

我根本没有更改默认的GC设置。据GCSettings称,LatencyMode Interactive和IsServerGC的所有运行都是假的。

我可以在每次调用Parallel.ForEach之前切换到LowLatency,但我真的更愿意了解发生了什么。

在VS2015升级后,还有其他人在Parallel.ForEach中看到过奇怪的冻结吗?关于下一步会有什么好处的任何想法?

更新1:在上面的模糊解释中添加一些示例代码......

以下是一些示例代码,我希望能够证明这个问题。此代码在B机器上运行10-12秒,始终如一。它遇到了许多Gen2系列,但它们几乎没有时间。如果我取消注释两个GC设置行,我可以强制它没有Gen2集合。它比30-50秒慢一些。

现在在我的A机器上,代码需要一段随机的时间。似乎是5到30分钟。它似乎变得更糟,它遇到的Gen2系列越多。如果我取消注释两个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:只是为了对未来的读者提出意见......

此修补程序:https://support.microsoft.com/en-us/kb/3088957完全解决了该问题。申请后我根本没有看到任何缓慢的问题。

事实证明没有与Parallel.ForEach有任何关系我相信基于此:http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx虽然修补程序确实提到Parallel.ForEach由于某种原因。

3 个答案:

答案 0 :(得分:27)

这确实表现得非常糟糕,背景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秒钟在我的机器上。

gen#1集合当然不需要那么长时间,你在这里看到的是第1代收集等待后台GC完成它的工作。换句话说,它实际上是背景GC需要6秒。背景GC只有在gen#0和gen#1段中的空间足够大而不需要gen#2集合时才能有效,而后台GC处于循环状态。不是这个应用程序的工作方式,它以非常高的速度吃内存。你看到的小峰值是多个任务被解锁,能够再次分配数组。当gen#1集合必须再次等待背景GC时,快速停止。

值得注意的是,此代码的分配模式对GC非常不友好。它将长寿命数组(dr.DerivedValues)与短期数组(tempArray)交错。当压缩堆时GC给了很多工作,每个分配的数组最终都会被移动。

.NET 4.6 GC中的明显缺陷是后台集合似乎永远不会有效地压缩堆。 看起来就像它一遍又一遍地完成工作一样,好像之前的集合根本没有紧凑。无论是设计还是错误都很难说,我再也没有干净的4.5机器了。我当然倾向于bug。您应该在connect.microsoft.com上报告此问题,以便Microsoft查看它。

解决方法非常容易实现,您所要做的就是防止长期和短期对象的尴尬间隔。你通过预先分配它们来做到这一点:

    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。

更新:this blog post确认了GC错误。修复即将推出。

更新:a hotfix was released

更新:在.NET 4.6.1中修复

答案 1 :(得分:10)

我们(和其他用户)遇到过类似的问题。我们通过在应用程序的app.config中禁用后台GC来解决它。请参阅https://connect.microsoft.com/VisualStudio/Feedback/Details/1594775的评论中的讨论。

gcConcurrent(非并发工作站GC)的app.config

<?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>

答案 2 :(得分:5)