Sae*_*ari 28 .net c# multithreading multiprocessing task-parallel-library
我编写了一个非常简单的"字数统计"程序,它读取文件并计算文件中每个单词的出现次数.这是代码的一部分:
class Alaki
{
private static List<string> input = new List<string>();
private static void exec(int threadcount)
{
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = threadcount;
Parallel.ForEach(Partitioner.Create(0, input.Count),options, (range) =>
{
var dic = new Dictionary<string, List<int>>();
for (int i = range.Item1; i < range.Item2; i++)
{
//make some delay!
//for (int x = 0; x < 400000; x++) ;
var tokens = input[i].Split();
foreach (var token in tokens)
{
if (!dic.ContainsKey(token))
dic[token] = new List<int>();
dic[token].Add(1);
}
}
});
}
public static void Main(String[] args)
{
StreamReader reader=new StreamReader((@"c:\txt-set\agg.txt"));
while(true)
{
var line=reader.ReadLine();
if(line==null)
break;
input.Add(line);
}
DateTime t0 = DateTime.Now;
exec(Environment.ProcessorCount);
Console.WriteLine("Parallel: " + (DateTime.Now - t0));
t0 = DateTime.Now;
exec(1);
Console.WriteLine("Serial: " + (DateTime.Now - t0));
}
}
Run Code Online (Sandbox Code Playgroud)
它简单直接.我使用字典来计算每个单词的出现次数.该样式大致基于MapReduce编程模型.如您所见,每个任务都使用自己的私有字典.所以,没有共享变量; 只是一堆自己计算单词的任务.以下是代码在四核i7 CPU上运行时的输出:
平行:00:00:01.6220927
序列号:00:00:02.0471171
加速大约是1.25,这意味着一场悲剧!但是当我在处理每一行时添加一些延迟时,我可以达到大约4的加速值.
在没有延迟的原始并行执行中,CPU的利用率几乎达不到30%,因此加速并不乐观.但是,当我们添加一些延迟时,CPU的利用率达到97%.
首先,我认为原因是程序的IO绑定性质(但我认为插入字典在某种程度上是CPU密集型的)并且它似乎是合乎逻辑的,因为所有线程都是从共享内存总线读取数据.然而,令人惊讶的是,当我同时运行4个串行程序实例(没有延迟)时,CPU的利用率达到大约提升,所有四个实例都在大约2.3秒内完成!
这意味着当代码在多处理配置中运行时,它达到大约3.5的加速值,但是当它在多线程配置中运行时,加速大约是1.25.
你有什么想法?我的代码有什么问题吗?因为我认为根本没有共享数据,我认为代码不会遇到任何争议..NET的运行时有缺陷吗?
提前致谢.
Bra*_*ger 53
Parallel.For不将输入分成n个(其中n是MaxDegreeOfParallelism); 相反,它创建了许多小批量,并确保最多n个正在同时处理.(这是因为如果一个批处理需要很长时间来处理,Parallel.For仍然可以在其他线程上运行.请参阅.NET中的Parallelism - 第5部分,分配工作以获取更多详细信息.)
由于这种设计,您的代码正在创建并丢弃数十个Dictionary对象,数百个List对象和数千个String对象.这给垃圾收集器带来了巨大的压力.
在我的计算机上运行PerfMonitor报告在GC中花费了43%的总运行时间.如果您重写代码以使用更少的临时对象,您应该看到所需的4倍加速.PerfMonitor报告的一些摘录如下:
超过总CPU时间的10%用于垃圾收集器.大多数调整良好的应用程序都在0-10%的范围内.这通常是由分配模式引起的,该分配模式允许对象生存足够长的时间以需要昂贵的Gen 2集合.
该程序的峰值GC堆分配率超过10 MB /秒.这是非常高的.这只是一个性能错误并不罕见.
编辑:根据您的评论,我将尝试解释您报告的时间.在我的计算机上,使用PerfMonitor,我在GC中花费了43%到52%的时间.为简单起见,我们假设50%的CPU时间是有效的,50%是GC.因此,如果我们使工作速度提高4倍(通过多线程)但保持GC的数量相同(这将发生,因为并行和串行配置中正在处理的批处理数量相同),最好我们可以获得的改进是原始时间的62.5%,或1.6倍.
但是,我们只看到1.25倍的加速,因为默认情况下GC不是多线程的(在工作站GC中).根据垃圾收集的基础知识,所有托管线程在Gen 0或Gen 1集合期间暂停.(并行和后台GC,在.NET 4和.NET 4.5中,可以在后台线程上收集Gen 2.)您的程序只有1.25倍的加速(并且您看到整体CPU使用率为30%),因为线程花费大部分时间暂停GC的时间(因为此测试程序的内存分配模式非常差).
如果启用服务器GC,它将在多个线程上执行垃圾回收.如果我这样做,程序运行速度提高2倍(几乎100%的CPU使用率).
当您同时运行程序的四个实例时,每个实例都有自己的托管堆,并且四个进程的垃圾回收可以并行执行.这就是为什么你看到100%的CPU使用率(每个进程使用100%的一个CPU).总体时间略长(所有的时间为2.3秒,一个为2.05秒)可能是由于测量不准确,磁盘争用,加载文件所需的时间,必须初始化线程池,上下文切换的开销或其他环境因素.
试图解释结果:
var dic = new Dictionary<string, List<int>>();
...
dic[token].Add(1);
Run Code Online (Sandbox Code Playgroud)
我替换了这个
var dic = new Dictionary<string, int>();
...
... else dic[token] += 1;
Run Code Online (Sandbox Code Playgroud)
结果更接近2倍加速.
但我的反问题是:这有关系吗?您的代码非常人为且不完整.并行版本最终会创建多个字典而不会合并它们.这甚至不是真实的情况.正如您所看到的,细节很重要.
您的示例代码很复杂,可以进行广泛的陈述Parallel.ForEach().
解决/分析真正的问题太简单了.