c# - .NET 多线程与多处理 : Awful Parallel. ForEach 性能

标签 c# .net multithreading task-parallel-library multiprocessing

我编写了一个非常简单的“字数统计”程序,它读取文件并计算文件中每个字的出现次数。这是代码的一部分:

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编程模型。如您所见,每个任务都在使用自己的私有(private)字典。所以,没有共享变量;只是一堆自己计算单词的任务。以下是代码在四核 i7 CPU 上运行时的输出:

并行:00:00:01.6220927
序列号:00:00:02.0471171

加速比大约是1.25,悲剧了!但是当我在处理每一行时添加一些延迟时,我可以达到大约 4 的加速值。

在原来的无延迟并行执行中,CPU的利用率几乎没有达到30%,因此提速并不理想。但是,当我们添加一些延迟时,CPU 的利用率达到了 97%。

首先,我认为原因是程序的 IO 绑定(bind)性质(但我认为插入字典在某种程度上是 CPU 密集型的)并且这似乎合乎逻辑,因为所有线程都从共享内存总线读取数据.然而,令人惊讶的是,当我同时运行 4 个串行程序实例(没有延迟)时,CPU 的利用率达到了大约 raises,所有 4 个实例都在大约 2.3 秒内完成!

这意味着当代码在多处理配置中运行时,它达到了大约 3.5 的加速值,但是当它在多线程配置中运行时,加速约为 1.25。

你的想法是什么? 我的代码有什么问题吗?因为我认为根本没有共享数据,而且我认为代码不会遇到任何争用。 .NET 的运行时是否存在缺陷?

提前致谢。

最佳答案

Parallel.For 不会将输入分成 n block (其中 nMaxDegreeOfParallelism) ;相反,它会创建许多小批处理并确保最多 n 被同时处理。 (这样一来,如果一个批处理需要很长时间才能处理,Parallel.For 仍可以在其他线程上运行。有关详细信息,请参阅 Parallelism in .NET - Part 5, Partioning of Work。)

由于这种设计,您的代码正在创建和丢弃数十个 Dictionary 对象、数百个 List 对象和数千个 String 对象。这给垃圾收集器带来了巨大的压力。

正在运行 PerfMonitor在我的电脑上报告说总运行时间的 43% 花在了 GC 上。如果您重写代码以使用更少的临时对象,您应该会看到所需的 4 倍加速。 PerfMonitor 报告的一些摘录如下:

Over 10% of the total CPU time was spent in the garbage collector. Most well tuned applications are in the 0-10% range. This is typically caused by an allocation pattern that allows objects to live just long enough to require an expensive Gen 2 collection.

This program had a peak GC heap allocation rate of over 10 MB/sec. This is quite high. It is not uncommon that this is simply a performance bug.

编辑:根据您的评论,我将尝试解释您报告的时间。在我的计算机上,使用 PerfMonitor,我测得 43% 到 52% 的时间花费在 GC 上。为简单起见,我们假设 50% 的 CPU 时间用于工作,50% 用于 GC。因此,如果我们使工作速度提高 4 倍(通过多线程)但保持 GC 的数量相同(这会发生,因为在并行和串行配置中处理的批处理数量恰好相同),最好的我们可以获得的改进是原始时间的 62.5%,即 1.6 倍。

但是,我们只看到了 1.25 倍的加速,因为默认情况下 GC 不是多线程的(在工作站 GC 中)。根据 Fundamentals of Garbage Collection ,所有托管线程在 Gen 0 或 Gen 1 收集期间暂停。 (并发和后台 GC,在 .NET 4 和 .NET 4.5 中,可以在后台线程上收集第 2 代。)你的程序只经历了 1.25× 加速(你看到 30% 的总体 CPU 使用率),因为线程花费了他们的大部分时间GC暂停时间(因为这个测试程序的内存分配模式很差)。

如果启用 server GC ,它将在多个线程上执行垃圾收集。如果我这样做,程序运行速度将提高 2 倍(CPU 使用率几乎为 100%)。

当您同时运行程序的四个实例时,每个实例都有自己的托管堆,并且四个进程的垃圾回收可以并行执行。这就是为什么您会看到 100% 的 CPU 使用率(每个进程使用一个 CPU 的 100%)。略长的总体时间(全部 2.3 秒 vs 一个 2.05 秒)可能是由于测量不准确、磁盘争用、加载文件花费的时间、必须初始化线程池、上下文切换的开销或其他一些原因环境因素。

关于c# - .NET 多线程与多处理 : Awful Parallel. ForEach 性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13671053/

相关文章:

c# - 如何在C#中从存储流中播放MP3?

C# API 返回 HTML 而不是 JSON

c# - 如何正确使用 ASP.NET Core 共享框架或者如何自己使用它的程序集?

c++ - 确定代码是否在特定线程中运行

c# - WPF 圆角 - 是否可以在拐角处使用一致的渐变?

c# - 更快的算法来改变位图中的色相/饱和度/亮度

c# - C++和C#中指针和函数参数的等价性

c++ - 处理器类型的运行时检测 - 原子操作

c++ - QObject: 没有这样的插槽 QThread::readyRead()

c# - 遍历字典对象