【问题标题】:PLINQ Performs Worse Than Usual LINQPLINQ 的性能比通常的 LINQ 差
【发布时间】:2010-07-28 15:30:56
【问题描述】:

令人惊讶的是,在我创建的一个小测试用例上,使用 PLINQ 并没有带来任何好处。事实上,它甚至比通常的 LINQ 还要糟糕。

这是测试代码:

    int repeatedCount = 10000000;
    private void button1_Click(object sender, EventArgs e)
    {
        var currTime = DateTime.Now;
        var strList = Enumerable.Repeat(10, repeatedCount);
        var result = strList.AsParallel().Sum();

        var currTime2 = DateTime.Now;
        textBox1.Text = (currTime2.Ticks-currTime.Ticks).ToString();

    }

    private void button2_Click(object sender, EventArgs e)
    {
        var currTime = DateTime.Now;
        var strList = Enumerable.Repeat(10, repeatedCount);
        var result = strList.Sum();

        var currTime2 = DateTime.Now;
        textBox2.Text = (currTime2.Ticks - currTime.Ticks).ToString();
    }

结果?

textbox1: 3437500
textbox2: 781250

所以,LINQ 比 PLINQ 花费更少的时间来完成类似的操作!

我做错了什么?还是有我不知道的转折?

编辑:我已经更新了我的代码以使用秒表,但同样的行为仍然存在。为了降低 JIT 的影响,我实际上尝试了几次同时单击 button1button2 并且没有特定的顺序。虽然我得到的时间可能不同,但定性行为仍然存在:PLINQ 在这种情况下确实比较慢。

【问题讨论】:

  • 提示:使用Stopwatch 类来衡量性能。测量时间段会比DateTime.Now更准确。
  • @Anthony Pegram,怎么会这样?我可以很容易地看到每种类型将是什么。
  • 你能告诉我们你在什么硬件上运行它吗?
  • @Anthony,抱歉命名不当;这只是我凭空想出的一个例子,一个简单的例子,所以我希望任何阅读它的人都能一眼就理解它。这里没有火箭科学。
  • @Anthony 肯定不是人民展览 A,因为 var 实际上不可能被滥用。如果您以有意义的方式命名变量,则无需知道类型即可理解代码。 (如果有疑问,请尝试阅读 F# 代码,它非常易读,您只能使用 var 的等效项(几乎至少))

标签: c# c#-4.0 plinq


【解决方案1】:

首先:停止使用 DateTime 来测量运行时间。请改用秒表。测试代码如下所示:

var watch = new Stopwatch();

var strList = Enumerable.Repeat(10, 10000000);

watch.Start();
var result = strList.Sum();
watch.Stop();

Console.WriteLine("Linear: {0}", watch.ElapsedMilliseconds);

watch.Reset();

watch.Start();
var parallelResult = strList.AsParallel().Sum();
watch.Stop();

Console.WriteLine("Parallel: {0}", watch.ElapsedMilliseconds);

Console.ReadKey();

第二:并行运行会增加开销。在这种情况下,PLINQ 必须找出划分集合的最佳方法,以便它可以安全地并行求和元素。之后,您需要加入来自创建的各种线程的结果并对它们进行求和。这不是一项简单的任务。

使用上面的代码,我可以看到使用 Sum() 会产生约 95 毫秒的调用。调用 .AsParallel().Sum() 大约 185 毫秒。

并行执行一项任务只有在您从中有所收获时才是一个好主意。在这种情况下,求和是一项非常简单的任务,您无法通过使用 PLINQ 获得。

【讨论】:

  • +1 @Justin Niessner 非常清晰的答案。 @Ngu Soon Hui PLINQ 会很方便,但如果你不理解它也会很邪恶。我建议您在开始使用 PLINQ 之前先查看 Cache False Sharing 和 GC Throttling。
  • 显然系统无法识别使用 PLINQ 速度较慢这一事实,这与本文stackoverflow.com/questions/2893637/… 中的声明相矛盾,即系统能够根据哪个更快来选择 LINQ 或 PLINQ。
  • @CuiPengFei - 很可能就是这样。无法保证每个平台/机器都以相同的性能运行代码。
【解决方案2】:

这是一个典型的错误——认为“我将运行一个简单的测试来比较这个单线程代码和这个多线程代码的性能。”

简单测试是您可以用来衡量多线程性能的最差类型的测试。

通常,并行化某些操作会产生性能优势当您并行化的步骤需要大量工作时。当步骤很简单时(例如,快速*),并行化工作的开销最终会使您获得的微不足道的性能提升相形见绌。


考虑这个类比。

您正在建造一座建筑物。如果你有一个工人,他必须一个接一个地砌砖,直到他砌好一堵墙,然后再砌下一堵墙,以此类推,直到所有的墙都建好并连接起来。这是一项缓慢而费力的任务,可以从并行化中受益。

做到这一点的正确方法是平行建造围墙——比如说,再雇用 3 名工人,并让每个工人建造自己的墙,这样可以同时建造4面墙。找到 3 名额外工人并分配他们的任务所花费的时间与之前建造 1 堵墙所需的时间相比,建造 4 堵墙所节省的时间是微不足道的。

错误的方法是并行砌砖——再雇佣大约一千名工人,让每个工人负责在时间。你可能会想,“如果一个工人每分钟可以砌2块砖,那么一千名工人每分钟应该可以砌2000块砖,所以我很快就会完成这项工作!”但现实情况是,通过在如此微观的层面上并行化你的工作量,你浪费了大量的精力来收集和协调你所有的工人,给他们分配任务(“把这块砖放在那里”),确保没有人工作正在干扰其他人的工作,等等。

所以这个类比的寓意是:通常,使用并行化来拆分实质性工作单元(如墙),但保留非实质性单元(如砖块)以通常的顺序方式处理。


*因此,您实际上可以通过采用任何快速执行代码并添加Thread.Sleep(100)(或其他一些随机数) 到最后。突然之间,这段代码的顺序执行每次迭代都会减慢 100 毫秒,而并行执行的速度会明显减少。

【讨论】:

    【解决方案3】:

    其他人指出了您的基准测试中的一些缺陷。这是一个简短的控制台应用程序,使其更简单:

    using System;
    using System.Diagnostics;
    using System.Linq;
    
    public class Test
    {
        const int Iterations = 1000000000;
    
        static void Main()
        {
            // Make sure everything's JITted
            Time(Sequential, 1);
            Time(Parallel, 1);
            Time(Parallel2, 1);
            // Now run the real tests
            Time(Sequential, Iterations);
            Time(Parallel,   Iterations);
            Time(Parallel2,  Iterations);
        }
    
        static void Time(Func<int, int> action, int count)
        {
            GC.Collect();
            Stopwatch sw = Stopwatch.StartNew();
            int check = action(count);
            if (count != check)
            {
                Console.WriteLine("Check for {0} failed!", action.Method.Name);
            }
            sw.Stop();
            Console.WriteLine("Time for {0} with count={1}: {2}ms",
                              action.Method.Name, count,
                              (long) sw.ElapsedMilliseconds);
        }
    
        static int Sequential(int count)
        {
            var strList = Enumerable.Repeat(1, count);
            return strList.Sum();
        }
    
        static int Parallel(int count)
        {
            var strList = Enumerable.Repeat(1, count);
            return strList.AsParallel().Sum();
        }
    
        static int Parallel2(int count)
        {
            var strList = ParallelEnumerable.Repeat(1, count);
            return strList.Sum();
        }
    }
    

    编译:

    csc /o+ /debug- Test.cs
    

    我的四核 i7 笔记本电脑上的结果;最多可以快速运行 2 个内核,或者更慢地运行 4 个内核。基本上ParallelEnumerable.Repeat 获胜,其次是序列版本,然后是普通Enumerable.Repeat 并行化。

    Time for Sequential with count=1: 117ms
    Time for Parallel with count=1: 181ms
    Time for Parallel2 with count=1: 12ms
    Time for Sequential with count=1000000000: 9152ms
    Time for Parallel with count=1000000000: 44144ms
    Time for Parallel2 with count=1000000000: 3154ms
    

    请注意,此答案的早期版本因元素数量错误而存在令人尴尬的缺陷 - 我对上述结果更有信心。

    【讨论】:

    • 我看不出你的基准和我的基准有任何根本差异,但为什么我们的结果大不相同?
    • @Ngu:您是在调试器下运行代码还是使用 VS2010 的 Intellitrace 功能?这些可能会极大地扭曲性能时序的结果。理想情况下,尝试在命令行上运行发布版本 - 而不是从 VS。
    • @Ngu: 1) 我的没有在 UI 上下文中运行。我不知道这是否有什么不同,但为什么要给基准做更多的工作呢? 2)我的显然是从等式中删除了 JIT。 3) 我的任务让任务做更多的工作(100 倍)。秒表/日期时间的差异是的红鲱鱼,国际海事组织 - 当你有相当长的时间段(多秒)时,日期时间的不精确性将是微不足道的。跨度>
    • 有趣。我添加了一些代码以确保在我的控制台应用程序中以及在我的 Core 2 Duo P8600 中所有内容都是 JIT 的,AsParallel 的运行速度仍然慢了近两倍。然后我尝试了您的控制台应用程序并看到与使用我的相同的结果......
    • @Justin:你的意思是我的应用在你的盒子上显示较慢的并行结果吗?你几点钟了?
    【解决方案4】:

    您是否可能没有考虑 JIT 时间?您应该运行两次测试并丢弃第一组结果。

    另外,您不应该使用 DateTime 来获取性能计时,而是使用 Stopwatch 类:

    var swatch = new Stopwatch();
    swatch.StartNew();
    
    var strList = Enumerable.Repeat(10, repeatedCount); 
    var result = strList.AsParallel().Sum(); 
    
    swatch.Stop();
    textBox1.Text = swatch.Elapsed;
    

    PLINQ 确实为序列的处理增加了一些开销。但是,您的案例中的巨大差异似乎过大了。当在多个内核/CPU 上运行逻辑的好处超过了开销成本时,PLINQ 才有意义。如果您没有多核,并行运行处理并没有真正的优势 - PLINQ 应该检测到这种情况并按顺序执行处理。

    编辑:在创建此类嵌入式性能测试时,您应该确保您没有在调试器下运行它们,或者没有启用 Intellitrace,因为它们会严重影响性能时序。

    【讨论】:

      【解决方案5】:

      我没有看到的更重要的一点是 .AsParallel 将根据所使用的集合具有不同的性能。

      在我的测试中,当不在 IEnumerable (Enumerable.Repeat) 上使用时,PLINQ 比 LINQ

        29ms  PLINQ  ParralelQuery    
        30ms   LINQ  ParralelQuery    
        30ms  PLINQ  Array
        38ms  PLINQ  List    
       163ms   LINQ  IEnumerable
       211ms   LINQ  Array
       213ms   LINQ  List
       273ms  PLINQ  IEnumerable
      4 processors
      

      代码是用 VB 编写的,但提供该代码是为了表明使用 .ToArray 可使 PLINQ 版本快几倍

          Dim test = Function(LINQ As Action, PLINQ As Action, type As String)
                         Dim sw1 = Stopwatch.StartNew : LINQ() : Dim ts1 = sw1.ElapsedMilliseconds
                         Dim sw2 = Stopwatch.StartNew : PLINQ() : Dim ts2 = sw2.ElapsedMilliseconds
                         Return {String.Format("{0,4}ms   LINQ  {1}", ts1, type), String.Format("{0,4}ms  PLINQ  {1}", ts2, type)}
                     End Function
      
          Dim results = New List(Of String) From {Environment.ProcessorCount & " processors"}
          Dim count = 12345678, iList = Enumerable.Repeat(1, count)
      
          With iList : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "IEnumerable")) : End With
          With iList.ToArray : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "Array")) : End With
          With iList.ToList : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "List")) : End With
          With ParallelEnumerable.Repeat(1, count) : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "ParralelQuery")) : End With
      
          MessageBox.Show(String.join(Environment.NewLine, From l In results Order By l))
      

      以不同的顺序运行测试会产生一些不同的结果,因此将它们放在一行中可以让我更容易上下移动它们。

      【讨论】:

        【解决方案6】:

        确实可能是这种情况,因为您正在增加上下文切换的数量,并且您没有执行任何有利于让线程等待诸如 i/o 完成之类的操作的操作。如果您在单个 cpu 盒中运行,情况会更糟。

        【讨论】:

          【解决方案7】:

          我建议将 Stopwatch 类用于计时指标。在您的情况下,它是间隔的更好度量。

          【讨论】:

            【解决方案8】:

            请阅读本文的副作用部分。

            http://msdn.microsoft.com/en-us/magazine/cc163329.aspx

            我认为您可能会遇到许多情况,其中 PLINQ 具有您必须了解的其他数据处理模式,然后您才选择认为它总是具有更快的响应时间。

            【讨论】:

              【解决方案9】:

              贾斯汀关于开销的评论是完全正确的。

              除了使用 PLINQ 之外,在编写一般并发软件时需要考虑的事项:

              您始终需要考虑工作项的“粒度”。有些问题非常适合并行化,因为它们可以在非常高的级别上“分块”,例如光线追踪整个并发帧(这类问题被称为令人尴尬的并行)。当有非常大的工作“块”时,与您想要完成的实际工作相比,创建和管理多个线程的开销可以忽略不计。

              PLINQ 使并发编程更容易,但这并不意味着您可以忽略对工作粒度的考虑。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2020-02-22
                • 2016-09-05
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2023-03-26
                • 1970-01-01
                相关资源
                最近更新 更多