gpt4 book ai didi

c# - VS2015升级后的垃圾回收和Parallel.ForEach问题

转载 作者:IT王子 更新时间:2023-10-29 04:05:02 25 4
gpt4 key购买 nike

我有一些代码可以在我自己的类 R 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 调试器有关)
  • 无论是在 Debug 还是 Release 模式下构建,都会发生卡住
  • 如果我以 .NET 4.5 或 .NET 4.6 为目标,则会发生卡住
  • 我尝试禁用 RyuJIT 但这并没有影响卡住

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

我可以在每次调用 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:只是为了将评论中的内容移出以供 future 的读者使用...

此修补程序: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。

最佳答案

这确实表现得太差了,后台 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 秒。仅当 gen #0 和 gen #1 段中的空间足够大以在后台 GC 运行时不需要 gen #2 收集时,后台 GC 才有效。不是这个应用程序的工作方式,它以非常高的速度消耗内存。您看到的小峰值是多个任务被解除阻塞,能够再次分配数组。当第 1 代收集必须再次等待后台 GC 时,快速停止。

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

.NET 4.6 GC 中的明显缺陷是后台收集似乎从未有效地压缩堆。 看起来它一遍又一遍地完成这项工作,就好像上一个集合根本没有压缩一样。很难说这是设计使然还是错误,我再也没有干净的 4.5 机器了。我当然倾向于错误。您应该在 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 中修复

关于c# - VS2015升级后的垃圾回收和Parallel.ForEach问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31747992/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com