.NET的多线程VS多处理:可怕Parallel.ForEach性能(.NET's Mult

2019-07-04 09:58发布

我已经编写了一个非常简单的“字数”程序,读取一个文件,并在文件中计算每个单词的出现。 下面是代码的一部分:

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));
    }
}

它是简单和直截了当。 我使用字典来计算每个单词的出现。 风格大致基于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的运行时的一个漏洞?

提前致谢。

Answer 1:

Parallel.For不分割输入到n个(其中nMaxDegreeOfParallelism ); 相反,它创造了许多小批量,并确保最多n被同时处理。 (这是这样,如果一个批次需要很长的时间来处理, Parallel.For仍可以在其它线程运行的工作见。 并行在.NET - 5部分,工作Partioning 。更多细节)

由于这种设计,你的代码是创建和扔掉几十Dictionary对象,数百名单的对象,以及数以千计的String对象。 这是把垃圾收集器的巨大压力。

运行PerfMonitor我的电脑上报告说,总运行时间43%在GC花费。 如果你重写代码使用的临时对象更少,你应该看到预期的4倍的速度提升。 从PerfMonitor报告如下一些摘录:

总CPU时间的10%以上是在垃圾收集器花费。 最良好调节的应用是在0-10%范围内。 这通常是通过分配模式,使对象就住在足够长的时间需要昂贵的Gen 2的集合引起的。

这个程序有超过10 MB /秒的峰值GC堆分配率。 这是相当高的。 这种情况并不少见,这是一个简单的性能缺陷。

编辑:根据您的意见,我会试图解释您报告的时间安排。 在我的电脑,用PerfMonitor,我43%,在GC上花费的时间52%之间测量。 为简单起见,我们假设的CPU时间的50%是工作,而50%是GC。 因此,如果我们做的工作4×更快(通过多线程),但保持GC相同的量(这不会发生,因为正在处理的批次数量正好是在并行和串行配置的相同),最佳改善我们可以得到的是原时间62.5%,或1.6倍。

然而,我们看到的只是一个1.25×加速,因为GC是不是默认多线程(在工作站GC)。 按垃圾收集的基础 ,所有托管线程一个第0级或第1代收集过程中暂停。 (并行和背景GC,在.NET 4和.NET 4.5,可以在后台线程收集的Gen 2)你的程序的经验只有1.25×加速(和你看到的30%的CPU使用率总体),因为线程花费其大部分时间被暂停用于GC(因为这个测试程序的内存分配模式是非常差)。

如果启用服务器GC ,它会在多个线程执行垃圾收集。 如果我这样做,程序运行2×更快(几乎100%的CPU使用率)。

当你同时运行该程序的四个实例,每个都有自己的托管堆和垃圾收集四个过程可以并行执行。 这就是为什么你看到100%的CPU使用率(每道工序都使用一个CPU的100%)。 稍长的总时间(达到2.3s所有VS 2.05s为一个)可能是由于在测量不准确,争用磁盘,加载该文件所花费的时间,具有初始化线程池,上下文切换开销,或一些其它环境因素。



Answer 2:

试图解释的结果:

  • 在VS探查一个快速运行表明它几乎达到了40%的CPU使用率。
  • String.Split是主要热点。
  • 因此共享的东西必须是阻塞的CPU。
  • 的东西是最有可能的内存分配。 您的瓶颈
var dic = new Dictionary<string, List<int>>();
...
   dic[token].Add(1);

我换成这与

var dic = new Dictionary<string, int>();
...
... else dic[token] += 1;

其结果是越接近2倍的加速。

但我想反问的问题是:什么关系呢? 你的代码是非常人为的,不完整的。 水货版本最终没有将它们合并创建多个词典。 这甚至还没有接近真实的情况。 正如你所看到的,小细节做的事。

示例代码是复杂的,使约广泛的声明Parallel.ForEach()
这是太简单的解决/分析的一个现实问题。



Answer 3:

只是为了好玩,这里是一个短PLINQ版本:

File.ReadAllText("big.txt").Split().AsParallel().GroupBy(t => t)
                                                .ToDictionary(g => g.Key, g => g.Count());


文章来源: .NET's Multi-threading vs Multi-processing: Awful Parallel.ForEach Performance