PLINQ执行比LINQ更糟糕

时间:2010-07-28 15:30:57

标签: c# c#-4.0 plinq

令人惊讶的是,使用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确实较慢。

9 个答案:

答案 0 :(得分:22)

这是一个经典的错误 - 想一想,“我将运行一个简单的测试来比较这个单线程代码与这个多线程代码的性能。”

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

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


考虑这个比喻。

你正在建造一座建筑物。如果你有一个工人,他必须逐个铺砖,直到他做了一面墙,然后为下一面墙做同样的事情,依此类推,直到所有的墙都建成并连接起来。这是一项缓慢而费力的任务,可以从并行化中受益。

正确这样做的方法是并行化墙建筑 - 雇用,比如再雇佣3名工人,并让每个工人构建自己的墙,以便可以同时建造4面墙。找到3名额外的工作人员并为他们分配任务所花费的时间与通过在以前建造1所花费的时间内获得4个墙壁所获得的节省相比是微不足道的。

错误这样做的方法是并行化砖砌 - 雇用大约一千多名工人,并让每个工人负责在一个砖上铺一块砖时间。你可能会想,“如果一个工人每分钟可以铺2块砖,那么一千名工人应该能够每分钟铺设2000块砖,所以我马上就能完成这项工作!”但实际情况是,通过在如此微观的层面上平衡你的工作量,你浪费了大量的能量收集和协调所有工人,为他们分配任务(“把这块砖放在那里”),确保没有人工作正在干扰别人的等等。

因此,这种类比的道德是:一般来说,使用并行化来分割实质性工作单元(如墙),但保留非实体单位(如砖块)以通常的顺序方式处理。


*因此,通过采用任何快速执行的代码并添加Thread.Sleep(100)(或其他一些代码),您可以在更加工作密集的上下文中实现并行化性能增益的非常接近的近似值随机数)到它的结尾。每次迭代突然顺序执行此代码的速度将减慢100 ms,而并行执行速度将显着降低。

答案 1 :(得分:21)

首先:停止使用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()可以调用~95ms。调用.AsParallel()。Sum()网络约为185ms。

如果通过这样做获得某些东西,那么在并行中执行任务只是一个好主意。在这种情况下,Sum是一个足够简单的任务,使用PLINQ无法获得。

答案 2 :(得分:8)

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

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

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

答案 3 :(得分:1)

你有没有考虑到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,因为这些会严重影响性能时序。

答案 4 :(得分:1)

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

在我的测试中,当IEnumerable(unset($master); )上没有使用时,PLINQ比LINQ 更快

Enumerable.Repeat

代码在VB中,但提供用于显示使用.ToArray使PLINQ版本快几倍

  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

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

答案 5 :(得分:0)

确实可能是这种情况,因为您正在增加上下文切换的数量,并且您没有执行任何可以使线程等待I / O完成等操作的操作。如果你在一个CPU盒中运行,情况会更糟。

答案 6 :(得分:0)

我建议使用秒表类来计时。在你的情况下,它是衡量间隔的更好方法。

答案 7 :(得分:0)

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

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

我认为你可以遇到许多条件,其中PLINQ具有你必须了解的额外数据处理模式,然后你选择认为它总是纯粹具有更快的响应时间。

答案 8 :(得分:0)

Justin对开销的评论是完全正确的。

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

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

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