13

令人惊讶的是,使用 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 在这种情况下确实比较慢。

4

9 回答 9

23

第一:停止使用 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 毫秒。

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

于 2010-07-28T15:33:35.903 回答
22

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

简单测试是衡量多线程性能的最差测试。

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


考虑这个类比。

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

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

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

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


*出于这个原因,您实际上可以通过获取任何快速执行的代码并将Thread.Sleep(100)(或其他一些随机数)添加到它的末尾,从而在更工作密集的环境中对并行化的性能增益做出相当好的近似。突然,此代码的顺序执行每次迭代将减慢 100 毫秒,而并行执行的减慢速度将明显减少。

于 2010-07-28T16:17:34.797 回答
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获胜,其次是序列版本,然后是并行化 normal 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

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

于 2010-07-28T15:44:52.790 回答
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,因为它们会显着扭曲性能时序。

于 2010-07-28T15:32:28.757 回答
1

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

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

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

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

于 2016-02-11T00:18:24.110 回答
0

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

于 2010-07-28T15:34:32.077 回答
0

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

于 2010-07-28T15:35:14.740 回答
0

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

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

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

于 2010-07-28T15:41:15.457 回答
0

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

除了使用 PLINQ 之外,通常在编写并发软件时需要考虑以下几点:

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

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

于 2010-07-28T16:05:41.120 回答