PLINQ执行比常规LINQ更糟糕

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

第一:停止使用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无法获得。

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

一个简单的测试是您可以运行以测量multithreading性能的最差测试。

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


考虑这个比喻。

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

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

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

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


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

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

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

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

您是否有可能不考虑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,因为这些会严重影响性能计时。

我没有看到提到的更重要的是.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)) 

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

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

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

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

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

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

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

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

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

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