Parallel.ForEach比foreach慢

41

这是代码:

using (var context = new AventureWorksDataContext())
{
    IEnumerable<Customer> _customerQuery = from c in context.Customers
                                           where c.FirstName.StartsWith("A")
                                           select c;

    var watch = new Stopwatch();
    watch.Start();

    var result = Parallel.ForEach(_customerQuery, c => Console.WriteLine(c.FirstName));

    watch.Stop();
    Debug.WriteLine(watch.ElapsedMilliseconds);

    watch = new Stopwatch();
    watch.Start();

    foreach (var customer in _customerQuery)
    {
        Console.WriteLine(customer.FirstName);
    }

    watch.Stop();
    Debug.WriteLine(watch.ElapsedMilliseconds);
}

问题在于,Parallel.ForEach 花费约400毫秒,而常规的 foreach 只需要约40毫秒。我到底做错了什么,为什么它不能按照我的期望工作?


11
基本上是因为涉及到设置成本,而且循环内部的工作量不足以证明开销。例如请参考此答案。(我预计这是一个重复的问题。) - Rup
2
Console.WriteLine() 让它完全无关紧要。 - H H
尝试移除 Console.WriteLine() 并替换为 c.FirstName = c.FirstName.ToLowerInvariant()。如果您的集合大约有5000个项目,则不会看到任何区别;但是,如果您的集合有6000、7000、10000个项目,在4核处理器上,您将会看到一个很大的差异(Parallel.Foreach 将更快)。 - Junior Mayhé
6个回答

199
假设您有一个任务要完成。比如说,您是一名数学老师,需要批改二十份试卷。每份试卷需要两分钟,所以大约需要四十分钟。
现在假设您决定雇用一些助手来帮助您批改试卷。找到四个助手需要一个小时的时间。你们每个人负责四份试卷,总共只花了八分钟就完成了任务。您交换了40分钟的工作时间,但总共需要68分钟的工作时间,包括额外花费的一个小时去找助手,因此这并不是节省时间的方法。找助手的开销比自己完成工作的成本更高。
现在假设您需要批改两万份试卷,大约需要40000分钟。如果您花一个小时的时间去找助手,那就是赚了。您和助手们每个人负责4000份试卷,总共只需要8060分钟就完成了任务,而不是40000分钟,相当于节省了近5倍的时间。找助手的开销基本上可以忽略不计。
并行化不是免费的。将工作分配给不同的线程的成本需要远小于每个线程执行的工作量。
进一步阅读: 阿姆达尔定律 引用:
给出在固定工作负载下可以预期的系统执行延迟的理论加速比,该系统的资源得到改进。 古斯塔夫森定律 给定执行时间的情况下,当系统资源得到提升时,可以期望系统在执行任务时延迟速度理论上的加速。

9
在成为出色的开发者之前,你首先要成为一名优秀的作家。 - Ayub

12

你应该意识到的第一件事是,并非所有的并行操作都有利。并行操作需要进行一定的管理开销,这个开销可能会根据并行化工作的复杂性而显得重要或者不重要。由于你的并行函数中的工作量很小,管理并行操作的开销变得比较重要,从而导致整体工作速度变慢。


10

为您的可枚举对象创建所有线程所带来的额外开销很可能是减慢速度的原因。 Parallel.ForEach 并不能一概而论地提高性能;需要权衡是否针对每个元素完成的操作可能会阻塞。

例如,如果您要进行 Web 请求或其他操作而不仅仅是向控制台写入数据,那么并行版本可能会更快。但是,仅仅将数据写入控制台是一个非常快的操作,因此创建线程和启动它们所带来的开销会使速度变慢。


6

正如之前的作者所说,使用Parallel.ForEach会有一些开销,但这不是你看不到性能提升的原因。由于Console.WriteLine是同步操作,因此一次只有一个线程在工作。尝试将主体改为非阻塞形式,您将会看到性能提升(只要主体中的工作量足够大以抵消开销)。


更准确地说:Console.WriteLine 是一个同步操作。 - Theodor Zoulias

1

我喜欢Salomon的回答,并想补充一点,你还需要额外考虑以下方面:

  1. 分配委托。
  2. 通过它们进行调用。

0
你也可以使用分区器来将任务分成大小合适的分区,以避免由于创建过多任务而产生的开销。 如何加快小循环体的速度 调整Partitioner.Create的第三个参数来决定分区的大小可能有助于提高性能。在我的案例中,我尝试将其设置为2个分区(分区大小=(总元素/ 2)+ 1),相比使用foreach循环执行简单任务,性能略有提升(提高了10%)。
请记住,对于非常简单的任务,像你的情况一样,这可能帮助不大,而且性能可能会比使用简单的foreach循环更低,正如之前的答案所指出的原因。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接