VS2015 升级后的垃圾收集和 Parallel.ForEach 问题
Garbage Collection and Parallel.ForEach Issue After VS2015 Upgrade
我有一些代码可以在我自己的 R-like C# DataFrame 中处理数百万行数据 class。有许多 Parallel.ForEach 调用用于并行迭代数据行。此代码已使用 VS2013 和 .NET 4.5 运行宁一年多没有问题。
我有两台开发机器(A 和 B),最近将机器 A 升级到 VS2015。我开始注意到我的代码中大约有一半时间出现 st运行ge 间歇性冻结。让它 运行 很长一段时间,事实证明代码最终确实完成了。只需 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,但我真的更想了解发生了什么。
有没有其他人看到 st运行ge 在 VS2015 升级后在 Parallel.ForEach 中冻结?关于下一步的好的想法是什么?
更新 1:在上面的模糊解释中添加一些示例代码...
这里有一些示例代码,我希望它能演示这个问题。此代码 运行s 在 B 机器上持续 10-12 秒。它遇到了一些 Gen2 集合,但它们几乎没有花费任何时间。如果我取消注释这两个 GC 设置行,我可以强制它没有 Gen2 收集。它比 30-50 秒要慢一些。
现在在我的 A 机器上,代码需要 运行dom 时间。似乎在 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。
这确实性能太差了,后台GC在这里对你不利。我注意到的第一件事是 Parallel.ForEach() 使用了太多任务。线程池管理器将线程行为误解为 "bogged down by 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 段中的 space 足够大以至于在后台 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 错误。即将修复。
更新:已在 .NET 4.6.1 中修复
我们(和其他用户)遇到了类似的问题。我们通过在应用程序 app.config 中禁用后台 GC 来解决这个问题。请参阅 https://connect.microsoft.com/VisualStudio/Feedback/Details/1594775.
评论中的讨论
app.config 用于 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
我有一些代码可以在我自己的 R-like C# DataFrame 中处理数百万行数据 class。有许多 Parallel.ForEach 调用用于并行迭代数据行。此代码已使用 VS2013 和 .NET 4.5 运行宁一年多没有问题。
我有两台开发机器(A 和 B),最近将机器 A 升级到 VS2015。我开始注意到我的代码中大约有一半时间出现 st运行ge 间歇性冻结。让它 运行 很长一段时间,事实证明代码最终确实完成了。只需 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,但我真的更想了解发生了什么。
有没有其他人看到 st运行ge 在 VS2015 升级后在 Parallel.ForEach 中冻结?关于下一步的好的想法是什么?
更新 1:在上面的模糊解释中添加一些示例代码...
这里有一些示例代码,我希望它能演示这个问题。此代码 运行s 在 B 机器上持续 10-12 秒。它遇到了一些 Gen2 集合,但它们几乎没有花费任何时间。如果我取消注释这两个 GC 设置行,我可以强制它没有 Gen2 收集。它比 30-50 秒要慢一些。
现在在我的 A 机器上,代码需要 运行dom 时间。似乎在 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。
这确实性能太差了,后台GC在这里对你不利。我注意到的第一件事是 Parallel.ForEach() 使用了太多任务。线程池管理器将线程行为误解为 "bogged down by 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 段中的 space 足够大以至于在后台 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 错误。即将修复。
更新:已在 .NET 4.6.1 中修复
我们(和其他用户)遇到了类似的问题。我们通过在应用程序 app.config 中禁用后台 GC 来解决这个问题。请参阅 https://connect.microsoft.com/VisualStudio/Feedback/Details/1594775.
评论中的讨论app.config 用于 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