我应该总是使用Parallel.Foreach,因为更多的线程必须加快一切吗?

时间:2010-11-13 13:23:37

标签: c# .net-4.0 foreach parallel-processing

对于每个正常的foreach使用parallel.foreach循环是否有意义?

我什么时候应该开始使用parallel.foreach,只迭代1,000,000个项目?

8 个答案:

答案 0 :(得分:66)

不,对每个foreach都没有意义。一些原因:

  • 您的代码可能实际上可以并行化。例如,如果您正在使用“到目前为止的结果”进行下一次迭代并且顺序很重要)
  • 如果您正在汇总(例如汇总值),那么有很多方法可以使用Parallel.ForEach,但您不应该盲目地这样做
  • 如果你的工作完成得非常快,那就没有任何好处,而且可能会慢下来

基本上没有在线程中应该盲目地完成。想想它实际上使 sense 并行化的地方。哦,并衡量影响,以确保利益值得增加复杂性。 (更难以进行调试。)TPL很棒,但不是免费的午餐。

答案 1 :(得分:19)

不,你绝对不应该这样做。这里重要的不是迭代次数,而是要完成的工作。如果您的工作非常简单,并行执行1000000个委托将增加巨大的开销,并且很可能比传统的单线程解决方案慢。您可以通过对数据进行分区来解决这个问题,因此您可以执行大量工作。

E.g。考虑以下情况:

Input = Enumerable.Range(1, Count).ToArray();
Result = new double[Count];

Parallel.ForEach(Input, (value, loopState, index) => { Result[index] = value*Math.PI; });

这里的操作非常简单,并行执行此操作的开销会使使用多个内核的收益相形见绌。此代码运行速度明显慢于常规foreach循环。

通过使用分区,我们可以减少开销并实际观察到性能的提升。

Parallel.ForEach(Partitioner.Create(0, Input.Length), range => {
   for (var index = range.Item1; index < range.Item2; index++) {
      Result[index] = Input[index]*Math.PI;
   }
});

这里的故事的士气是并行性很难,你应该在仔细观察眼前的情况后才采用这种方法。此外,您应该在添加并行性之前和之后分析代码。

请记住,无论任何潜在的性能增益,并行性总是会增加代码的复杂性,因此如果性能已经足够好,则没有理由增加复杂性。

答案 2 :(得分:13)

简短的回答是,你不应该只在每个循环上使用Parallel.ForEach或相关的结构。 并行有一些开销,这在具有很少,快速迭代的循环中是不合理的。此外,break在这些循环中明显更复杂。

Parallel.ForEach是根据循环中的迭代次数,硬件上的CPU核心数和该硬件上的当前负载,在任务调度程序认为合适时调度循环的请求。实际并行执行并不总能得到保证,如果内核数量较少,迭代次数较少和/或当前负载较高,则不太可能实现并行执行。

另请参阅Does Parallel.ForEach limits the number of active threads?Does Parallel.For use one Task per iteration?

答案很长:

我们可以根据它们落在两个轴上的方式对循环进行分类:

  1. 很少迭代到很多迭代。
  2. 每次迭代都很快,每次迭代都很慢。
  3. 第三个因素是如果任务的持续时间变化非常大 - 例如,如果您在Mandelbrot集合上计算点数,则某些点可以快速计算,有些点需要更长时间。

    当很少,快速迭代时,可能不值得以任何方式使用并行化,很可能由于开销而最终会变慢。即使并行化确实加速了特定的小型快速循环,也不太可能引起人们的兴趣:增益很小,并且它不是应用程序中的性能瓶颈,因此优化可读性而不是性能。

    如果一个循环只有非常少的,慢的迭代并且你想要更多的控制,你可以考虑使用Tasks来处理它们,如下所示:

    var tasks = new List<Task>(actions.Length); 
    foreach(var action in actions) 
    { 
        tasks.Add(Task.Factory.StartNew(action)); 
    } 
    Task.WaitAll(tasks.ToArray());
    

    如果迭代次数很多,Parallel.ForEach就在其元素中。

    Microsoft documentation表示

      

    当并行循环运行时,TPL会对数据源进行分区   循环可以同时在多个部分上运行。在...后面   在场景中,任务计划程序根据系统对任务进行分区   资源和工作量。如果可能,调度程序将重新分配   如果工作量变为多个线程和处理器之间的工作   不平衡。

    随着循环迭代次数的减少,这种分区和动态重新调度将更难以有效地完成,并且如果迭代的持续时间和同一台机器上运行的其他任务的存在不同,则更为必要。 / p>

    我运行了一些代码。

    下面的测试结果显示一台机器上没有其他任何东西在运行,并且没有来自.Net线程池的其他线程正在使用中。这不是典型的(实际上在Web服务器场景中,这是非常不现实的)。在实践中,您可能看不到任何具有少量迭代的并行化。

    测试代码是:

    namespace ParallelTests 
    { 
        class Program 
        { 
            private static int Fibonacci(int x) 
            { 
                if (x <= 1) 
                { 
                    return 1; 
                } 
                return Fibonacci(x - 1) + Fibonacci(x - 2); 
            } 
    
            private static void DummyWork() 
            { 
                var result = Fibonacci(10); 
                // inspect the result so it is no optimised away. 
                // We know that the exception is never thrown. The compiler does not. 
                if (result > 300) 
                { 
                    throw new Exception("failed to to it"); 
                } 
            } 
    
            private const int TotalWorkItems = 2000000; 
    
            private static void SerialWork(int outerWorkItems) 
            { 
                int innerLoopLimit = TotalWorkItems / outerWorkItems; 
                for (int index1 = 0; index1 < outerWorkItems; index1++) 
                { 
                    InnerLoop(innerLoopLimit); 
                } 
            } 
    
            private static void InnerLoop(int innerLoopLimit) 
            { 
                for (int index2 = 0; index2 < innerLoopLimit; index2++) 
                { 
                    DummyWork(); 
                } 
            } 
    
            private static void ParallelWork(int outerWorkItems) 
            { 
                int innerLoopLimit = TotalWorkItems / outerWorkItems; 
                var outerRange = Enumerable.Range(0, outerWorkItems); 
                Parallel.ForEach(outerRange, index1 => 
                { 
                    InnerLoop(innerLoopLimit); 
                }); 
            } 
    
            private static void TimeOperation(string desc, Action operation) 
            { 
                Stopwatch timer = new Stopwatch(); 
                timer.Start(); 
                operation(); 
                timer.Stop(); 
    
                string message = string.Format("{0} took {1:mm}:{1:ss}.{1:ff}", desc, timer.Elapsed); 
                Console.WriteLine(message); 
            } 
    
            static void Main(string[] args) 
            { 
                TimeOperation("serial work: 1", () => Program.SerialWork(1)); 
                TimeOperation("serial work: 2", () => Program.SerialWork(2)); 
                TimeOperation("serial work: 3", () => Program.SerialWork(3)); 
                TimeOperation("serial work: 4", () => Program.SerialWork(4)); 
                TimeOperation("serial work: 8", () => Program.SerialWork(8)); 
                TimeOperation("serial work: 16", () => Program.SerialWork(16)); 
                TimeOperation("serial work: 32", () => Program.SerialWork(32)); 
                TimeOperation("serial work: 1k", () => Program.SerialWork(1000)); 
                TimeOperation("serial work: 10k", () => Program.SerialWork(10000)); 
                TimeOperation("serial work: 100k", () => Program.SerialWork(100000)); 
    
                TimeOperation("parallel work: 1", () => Program.ParallelWork(1)); 
                TimeOperation("parallel work: 2", () => Program.ParallelWork(2)); 
                TimeOperation("parallel work: 3", () => Program.ParallelWork(3)); 
                TimeOperation("parallel work: 4", () => Program.ParallelWork(4)); 
                TimeOperation("parallel work: 8", () => Program.ParallelWork(8)); 
                TimeOperation("parallel work: 16", () => Program.ParallelWork(16)); 
                TimeOperation("parallel work: 32", () => Program.ParallelWork(32)); 
                TimeOperation("parallel work: 64", () => Program.ParallelWork(64)); 
                TimeOperation("parallel work: 1k", () => Program.ParallelWork(1000)); 
                TimeOperation("parallel work: 10k", () => Program.ParallelWork(10000)); 
                TimeOperation("parallel work: 100k", () => Program.ParallelWork(100000)); 
    
                Console.WriteLine("done"); 
                Console.ReadLine(); 
            } 
        } 
    } 
    

    4核Windows 7机器上的结果是:

    serial work: 1 took 00:02.31 
    serial work: 2 took 00:02.27 
    serial work: 3 took 00:02.28 
    serial work: 4 took 00:02.28 
    serial work: 8 took 00:02.28 
    serial work: 16 took 00:02.27 
    serial work: 32 took 00:02.27 
    serial work: 1k took 00:02.27 
    serial work: 10k took 00:02.28 
    serial work: 100k took 00:02.28 
    
    parallel work: 1 took 00:02.33 
    parallel work: 2 took 00:01.14 
    parallel work: 3 took 00:00.96 
    parallel work: 4 took 00:00.78 
    parallel work: 8 took 00:00.84 
    parallel work: 16 took 00:00.86 
    parallel work: 32 took 00:00.82 
    parallel work: 64 took 00:00.80 
    parallel work: 1k took 00:00.77 
    parallel work: 10k took 00:00.78 
    parallel work: 100k took 00:00.77 
    done
    

    运行代码在.Net 4和.Net 4.5中编译得到的结果大致相同。

    连续工作运行完全相同。如何切片它并不重要,它在大约2.28秒内运行。

    1次迭代的并行工作比没有并行性略长。 2个项目更短,因此是3个,并且4次或更多次迭代都是大约0.8秒。

    它正在使用所有核心,但不是100%的效率。如果连续工作分为4种方式而没有开销,则会在0.57秒内完成(2.28 / 4 = 0.57)。

    在其他情况下,我看到并行2-3次迭代完全没有加速。您没有对Parallel.ForEach进行细粒度控制,并且算法可能决定将它们“分区”为1个块,如果机器忙,则在1个核心上运行它。

答案 3 :(得分:9)

进行并行操作没有下限。如果您只有2个项目可以使用,但每个项目都需要一段时间,那么使用Parallel.ForEach可能仍然有意义。另一方面,如果你有1000000个项目,但它们做得不多,那么并行循环可能不会比常规循环快。

例如,我编写了一个简单的程序来计算嵌套循环,其中外部循环使用for循环和Parallel.ForEach运行。我把它放在我的4-CPU(双核,超线程)笔记本电脑上。

这是一个只有2个项目可以运行的运行,但每个项目需要一段时间:

2 outer iterations, 100000000 inner iterations:
for loop: 00:00:00.1460441
ForEach : 00:00:00.0842240

这是一个包含数百万个项目的运行,但它们并没有做太多的事情:

100000000 outer iterations, 2 inner iterations:
for loop: 00:00:00.0866330
ForEach : 00:00:02.1303315

唯一真正知道的方法是尝试。

答案 4 :(得分:1)

您不应该盲目地使用并行foreach替换应用程序中的每个foreach循环。更多线程并不意味着您的应用程序将更快地运行。如果您想真正从多个线程中受益,您需要将任务分割成可以并行运行的较小任务。如果您的算法不可并行化,您将无法获得任何好处。

答案 5 :(得分:1)

通常,一旦你超过每个核心的一个线程,一个操作中涉及的每个额外线程都会使它变慢,而不是更快。

但是,如果每个操作的一部分将被阻塞(经典示例正在等待磁盘或网络I / O,另一个是生产者和消费者彼此不同步)那么比核心更多的线程可以开始加速再次,因为任务可以在其他线程无法进行直到I / O操作返回时完成。

出于这个原因,当单核机器成为常态时,多线程中唯一真正的理由是当I / O引入的阻塞或者提高响应性时(执行任务稍慢)但是再次开始响应用户输入要快得多。)

尽管如此,目前单核机器的情况越来越少,因此看起来您应该能够通过并行处理将所有内容的速度提高至少两倍。

如果订单很重要,或者任务固有的某些事情迫使它产生同步瓶颈,或者如果操作次数太少以至于并行处理的速度提高超过了并行处理的速度,那么情况仍然不是这样。设置并行处理所涉及的开销。如果共享资源需要线程阻塞执行相同并行操作的其他线程(取决于锁争用程度),则可能会出现这种情况,也可能不是这种情况。

此外,如果您的代码本身就是多线程的,那么您可能处于这样一种情况:您本质上是在与自己竞争资源(经典案例是处理并发请求的ASP.NET代码)。这里并行操作的优点可能意味着4核机器上的单个测试操作接近性能的4倍,但是一旦需要执行相同任务的请求数达到4,那么由于这4个请求中的每一个都是试图使用每个核心,它变得比它们每个核心(可能稍微更好,可能稍差)更好。因此,当使用从单一请求测试变为现实世界的大量请求时,并行操作的好处就会消失。

答案 6 :(得分:0)

没有。您需要了解代码正在做什么以及它是否适合并行化。数据项之间的依赖关系可能很难并行化,即,如果一个线程使用为前一个元素计算的值,它必须等到该值仍然计算并且不能并行运行。您还需要了解目标架构,但是,您现在通常会在几乎所有购买的东西上安装多核CPU。即使在单核上,您也可以从更多线程中获得一些好处,但前提是您有一些阻塞任务。您还应该记住,创建和组织并行线程会产生开销。如果这个开销是你的任务所花费的时间的一小部分(或者更多),那么你可以放慢速度。

答案 7 :(得分:0)

这些是我的基准测试,显示纯串行最慢,以及各种级别的分区。

class Program
{
    static void Main(string[] args)
    {
        NativeDllCalls(true, 1, 400000000, 0);  // Seconds:     0.67 |)   595,203,995.01 ops
        NativeDllCalls(true, 1, 400000000, 3);  // Seconds:     0.91 |)   439,052,826.95 ops
        NativeDllCalls(true, 1, 400000000, 4);  // Seconds:     0.80 |)   501,224,491.43 ops
        NativeDllCalls(true, 1, 400000000, 8);  // Seconds:     0.63 |)   635,893,653.15 ops
        NativeDllCalls(true, 4, 100000000, 0);  // Seconds:     0.35 |) 1,149,359,562.48 ops
        NativeDllCalls(true, 400, 1000000, 0);  // Seconds:     0.24 |) 1,673,544,236.17 ops
        NativeDllCalls(true, 10000, 40000, 0);  // Seconds:     0.22 |) 1,826,379,772.84 ops
        NativeDllCalls(true, 40000, 10000, 0);  // Seconds:     0.21 |) 1,869,052,325.05 ops
        NativeDllCalls(true, 1000000, 400, 0);  // Seconds:     0.24 |) 1,652,797,628.57 ops
        NativeDllCalls(true, 100000000, 4, 0);  // Seconds:     0.31 |) 1,294,424,654.13 ops
        NativeDllCalls(true, 400000000, 0, 0);  // Seconds:     1.10 |)   364,277,890.12 ops
    }


static void NativeDllCalls(bool useStatic, int nonParallelIterations, int parallelIterations = 0, int maxParallelism = 0)
{
    if (useStatic) {
        Iterate<string, object>(
            (msg, cntxt) => { 
                ServiceContracts.ForNativeCall.SomeStaticCall(msg); 
            }
            , "test", null, nonParallelIterations,parallelIterations, maxParallelism );
    }
    else {
        var instance = new ServiceContracts.ForNativeCall();
        Iterate(
            (msg, cntxt) => {
                cntxt.SomeCall(msg);
            }
            , "test", instance, nonParallelIterations, parallelIterations, maxParallelism);
    }
}

static void Iterate<T, C>(Action<T, C> action, T testMessage, C context, int nonParallelIterations, int parallelIterations=0, int maxParallelism= 0)
{
    var start = DateTime.UtcNow;            
    if(nonParallelIterations == 0)
        nonParallelIterations = 1; // normalize values

    if(parallelIterations == 0)
        parallelIterations = 1; 

    if (parallelIterations > 1) {                    
        ParallelOptions options;
        if (maxParallelism == 0) // default max parallelism
            options = new ParallelOptions();
        else
            options = new ParallelOptions { MaxDegreeOfParallelism = maxParallelism };

        if (nonParallelIterations > 1) {
            Parallel.For(0, parallelIterations, options
            , (j) => {
                for (int i = 0; i < nonParallelIterations; ++i) {
                    action(testMessage, context);
                }
            });
        }
        else { // no nonParallel iterations
            Parallel.For(0, parallelIterations, options
            , (j) => {                        
                action(testMessage, context);
            });
        }
    }
    else {
        for (int i = 0; i < nonParallelIterations; ++i) {
            action(testMessage, context);
        }
    }

    var end = DateTime.UtcNow;

    Console.WriteLine("\tSeconds: {0,8:0.00} |) {1,16:0,000.00} ops",
        (end - start).TotalSeconds, (Math.Max(parallelIterations, 1) * nonParallelIterations / (end - start).TotalSeconds));

}

}