【发布时间】:2015-10-23 05:46:07
【问题描述】:
我有一些代码可以在我自己的类 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 调试器的问题)
- 无论我是在调试模式还是发布模式下构建,都会发生冻结
- 如果我以 .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:只是为了将内容从 cmets 中移出以供未来的读者使用...
此修补程序: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。
【问题讨论】:
-
下一步是发布MCVE,这样我们就可以尝试在我们的机器上重现它,看看我们是否遇到相同的行为。这是为作为 x86 或 x64 进程运行而构建的吗?
-
x64。明白了,正在做一个。但是很难让 GC 工作得恰到好处。希望我遗漏了一些明显的东西。
-
@MichaelCovelli 使用
GC.Collect()强制循环中的 GC 会发生什么? -
此修补程序:support.microsoft.com/en-us/kb/3088957 完全解决了该问题。申请后我根本没有看到任何缓慢的问题。
标签: c# garbage-collection visual-studio-2015 parallel.foreach .net-4.6