多线程程序不使用 100% CPU 用于昂贵的 IEnumerables
Multithreaded program NOT using 100% CPU for expensive IEnumerables
使用 IEnumerables 的多线程,并行计算多次并且计算成本高,不使用 100% CPU.示例是 Aggregate() 函数结合 Concat():
// Initialisation.
// Each IEnumerable<string> is made so that it takes time to evaluate it
// everytime when it is accessed.
IEnumerable<string>[] iEnumerablesArray = ...
// The line of the question (using less than 100% CPU):
Parallel.For(0, 1000000, _ => iEnumerablesArray.Aggregate(Enumerable.Concat).ToList());
问题:为什么并行计算 IEnumerables 多次的并行代码不使用 100% CPU?代码不使用锁或等待,所以这种行为是意外。 post.
末尾有完整的模拟代码
注释和编辑:
- 有趣的事实:如果代码
Enumerable.Range(0, 1).Select(__ => GenerateLongString())
末尾的完整代码的
改为
Enumerable.Range(0, 1).Select(__ => GenerateLongString()).ToArray().AsEnumerable()
,
然后初始化需要几秒钟,然后 CPU 用于 100%(没有问题发生)
- 有趣的事实 2:(来自评论)当方法
GenerateLongString()
在 GC 上变得不那么沉重而在 CPU 上更加密集时,那么 CPU 达到 100%。所以原因与这个方法的实现有关。但是,有趣的是,如果在没有 IEnumerable 的情况下调用 GenerateLongString()
的当前形式,CPU 也会达到 100%:
Parallel.For(0, int.MaxValue, _ => GenerateLongString());
所以 GenerateLongString()
的重量并不是这里唯一的问题。
- 事实 3:(来自评论)建议 concurrency visualiser 显示线程大部分时间在线
clr.dll!WKS::gc_heap::wait_for_gc_done
,
等待 GC 完成。这发生在 GenerateLongString()
的 string.Concat()
内。
- 当 运行 手动多个 Task.Factory.StartNew() 或 Thread.Start()
时观察到相同的行为
- 在 Win 10 和 Windows Server 2012
上观察到相同的行为
- 在真机和虚拟机上观察到相同的行为
- 发布与调试无关紧要。
- .Net 测试版本:4.7.2
完整代码:
class Program
{
const int DATA_SIZE = 10000;
const int IENUMERABLE_COUNT = 10000;
static void Main(string[] args)
{
// initialisation - takes milliseconds
IEnumerable<string>[] iEnumerablesArray = GenerateArrayOfIEnumerables();
Console.WriteLine("Initialized");
List<string> result = null;
// =================
// THE PROBLEM LINE:
// =================
// CPU usage of next line:
// - 40 % on 4 virtual cores processor (2 physical)
// - 10 - 15 % on 12 virtual cores processor
Parallel.For(
0,
int.MaxValue,
(i) => result = iEnumerablesArray.Aggregate(Enumerable.Concat).ToList());
// just to be sure that Release mode would not omit some lines:
Console.WriteLine(result);
}
static IEnumerable<string>[] GenerateArrayOfIEnumerables()
{
return Enumerable
.Range(0, IENUMERABLE_COUNT)
.Select(_ => Enumerable.Range(0, 1).Select(__ => GenerateLongString()))
.ToArray();
}
static string GenerateLongString()
{
return string.Concat(Enumerable.Range(0, DATA_SIZE).Select(_ => "string_part"));
}
}
您的线程在 clr.dll!WKS::gc_heap::wait_for_gc_done
上被阻塞的事实表明垃圾收集器是您应用程序的瓶颈。您应该尽可能地限制程序中堆分配的数量,以减轻 gc 的压力。
也就是说,还有另一种加快速度的方法。默认情况下,在桌面上,GC 配置为使用计算机上的有限资源(以避免减慢其他应用程序的速度)。如果你想充分利用可用的资源,那么你可以activate server GC。此模式假定您的应用程序是计算机上最重要的 运行。它将提供显着的性能提升,但使用更多 CPU 和内存。
使用 IEnumerables 的多线程,并行计算多次并且计算成本高,不使用 100% CPU.示例是 Aggregate() 函数结合 Concat():
// Initialisation.
// Each IEnumerable<string> is made so that it takes time to evaluate it
// everytime when it is accessed.
IEnumerable<string>[] iEnumerablesArray = ...
// The line of the question (using less than 100% CPU):
Parallel.For(0, 1000000, _ => iEnumerablesArray.Aggregate(Enumerable.Concat).ToList());
问题:为什么并行计算 IEnumerables 多次的并行代码不使用 100% CPU?代码不使用锁或等待,所以这种行为是意外。 post.
末尾有完整的模拟代码注释和编辑:
- 有趣的事实:如果代码
Enumerable.Range(0, 1).Select(__ => GenerateLongString())
末尾的完整代码的
改为Enumerable.Range(0, 1).Select(__ => GenerateLongString()).ToArray().AsEnumerable()
,
然后初始化需要几秒钟,然后 CPU 用于 100%(没有问题发生) - 有趣的事实 2:(来自评论)当方法
GenerateLongString()
在 GC 上变得不那么沉重而在 CPU 上更加密集时,那么 CPU 达到 100%。所以原因与这个方法的实现有关。但是,有趣的是,如果在没有 IEnumerable 的情况下调用GenerateLongString()
的当前形式,CPU 也会达到 100%:Parallel.For(0, int.MaxValue, _ => GenerateLongString());
所以GenerateLongString()
的重量并不是这里唯一的问题。 - 事实 3:(来自评论)建议 concurrency visualiser 显示线程大部分时间在线
clr.dll!WKS::gc_heap::wait_for_gc_done
,
等待 GC 完成。这发生在GenerateLongString()
的string.Concat()
内。 - 当 运行 手动多个 Task.Factory.StartNew() 或 Thread.Start() 时观察到相同的行为
- 在 Win 10 和 Windows Server 2012 上观察到相同的行为
- 在真机和虚拟机上观察到相同的行为
- 发布与调试无关紧要。
- .Net 测试版本:4.7.2
完整代码:
class Program
{
const int DATA_SIZE = 10000;
const int IENUMERABLE_COUNT = 10000;
static void Main(string[] args)
{
// initialisation - takes milliseconds
IEnumerable<string>[] iEnumerablesArray = GenerateArrayOfIEnumerables();
Console.WriteLine("Initialized");
List<string> result = null;
// =================
// THE PROBLEM LINE:
// =================
// CPU usage of next line:
// - 40 % on 4 virtual cores processor (2 physical)
// - 10 - 15 % on 12 virtual cores processor
Parallel.For(
0,
int.MaxValue,
(i) => result = iEnumerablesArray.Aggregate(Enumerable.Concat).ToList());
// just to be sure that Release mode would not omit some lines:
Console.WriteLine(result);
}
static IEnumerable<string>[] GenerateArrayOfIEnumerables()
{
return Enumerable
.Range(0, IENUMERABLE_COUNT)
.Select(_ => Enumerable.Range(0, 1).Select(__ => GenerateLongString()))
.ToArray();
}
static string GenerateLongString()
{
return string.Concat(Enumerable.Range(0, DATA_SIZE).Select(_ => "string_part"));
}
}
您的线程在 clr.dll!WKS::gc_heap::wait_for_gc_done
上被阻塞的事实表明垃圾收集器是您应用程序的瓶颈。您应该尽可能地限制程序中堆分配的数量,以减轻 gc 的压力。
也就是说,还有另一种加快速度的方法。默认情况下,在桌面上,GC 配置为使用计算机上的有限资源(以避免减慢其他应用程序的速度)。如果你想充分利用可用的资源,那么你可以activate server GC。此模式假定您的应用程序是计算机上最重要的 运行。它将提供显着的性能提升,但使用更多 CPU 和内存。