在多核机器上进行.NET操作的非线性扩展

时间:2009-09-20 00:14:00

标签: c# linq performance parallel-processing plinq

我在.NET应用程序中遇到一种奇怪的行为,它对一组内存数据执行一些高度并行的处理。

在多核处理器(IntelCore2 Quad Q6600 2.4GHz)上运行时,由于启动了多个线程来处理数据,因此它呈现出非线性缩放。

当作为单核上的非多线程循环运行时,该进程每秒能够完成大约240万次计算。当作为四个线程运行时,您可以预期吞吐量的四倍 - 在每秒900万次计算的某个地方 - 但是,唉,没有。在实践中,它每秒仅完成约4.1百万......与预期的吞吐量相当短。

此外,无论我使用PLINQ,线程池还是四个显式创建的线程,都会发生这种情况。很奇怪......

使用CPU时间在机器上没有其他任何东西在运行,计算中也没有涉及任何锁或其他同步对象...它应该直接通过数据。我已经通过在进程运行时查看perfmon数据来确认这一点(尽可能)......并且没有报告的线程争用或垃圾收集活动。

我的理论:

  1. 所有技术(线程上下文切换等)的开销都压倒了计算
  2. 线程没有被分配给四个核心中的每一个,并且花费一些时间等待同一处理器核心......不确定如何测试这个理论......
  3. .NET CLR线程没有按预期的优先级运行,或者有一些隐藏的内部开销。
  4. 以下是应该表现出相同行为的代码摘录:

        var evaluator = new LookupBasedEvaluator();
    
        // find all ten-vertex polygons that are a subset of the set of points
        var ssg = new SubsetGenerator<PolygonData>(Points.All, 10);
    
        const int TEST_SIZE = 10000000;  // evaluate the first 10 million records
    
        // materialize the data into memory...
        var polygons = ssg.AsParallel()
                          .Take(TEST_SIZE)
                          .Cast<PolygonData>()
                          .ToArray();
    
        var sw1 = Stopwatch.StartNew();
        // for loop completes in about 4.02 seconds... ~ 2.483 million/sec
        foreach( var polygon in polygons )
            evaluator.Evaluate(polygon);
        s1.Stop(); 
        Console.WriteLine( "Linear, single core loop: {0}", s1.ElapsedMilliseconds );
    
        // now attempt the same thing in parallel using Parallel.ForEach...
        // MS documentation indicates this internally uses a worker thread pool
        // completes in 2.61 seconds ... or ~ 3.831 million/sec
        var sw2 = Stopwatch.StartNew();
        Parallel.ForEach(polygons, p => evaluator.Evaluate(p));
        sw2.Stop();
        Console.WriteLine( "Parallel.ForEach() loop: {0}", s2.ElapsedMilliseconds );
    
        // now using PLINQ, er get slightly better results, but not by much
        // completes in 2.21 seconds ... or ~ 4.524 million/second
        var sw3 = Stopwatch.StartNew();
        polygons.AsParallel(Environment.ProcessorCount)
                .AsUnordered() // no sure this is necessary...
                .ForAll( h => evalautor.Evaluate(h) );
        sw3.Stop();
        Console.WriteLine( "PLINQ.AsParallel.ForAll: {0}", s3.EllapsedMilliseconds );
    
        // now using four explicit threads:
        // best, still short of expectations at 1.99 seconds = ~ 5 million/sec
        ParameterizedThreadStart tsd = delegate(object pset) { foreach (var p in (IEnumerable<Card[]>) pset) evaluator.Evaluate(p); };
         var t1 = new Thread(tsd);
         var t2 = new Thread(tsd);
         var t3 = new Thread(tsd);
         var t4 = new Thread(tsd);
    
         var sw4 = Stopwatch.StartNew(); 
         t1.Start(hands);
         t2.Start(hands);
         t3.Start(hands);
         t4.Start(hands);
         t1.Join();
         t2.Join();
         t3.Join();
         t4.Join();
         sw.Stop();
         Console.WriteLine( "Four Explicit Threads: {0}", s4.EllapsedMilliseconds );
    

4 个答案:

答案 0 :(得分:5)

看一下这篇文章:http://blogs.msdn.com/pfxteam/archive/2008/08/12/8849984.aspx

具体来说,限制并行区域中的内存分配,并仔细检查写入,以确保它们不会发生在其他线程读取或写入的内存位置附近。

答案 1 :(得分:5)

所以我终于弄明白问题是什么 - 我认为与SO社区分享是有用的。

非线性性能的整个问题是Evaluate()方法中单行的结果:

var coordMatrix = new long[100];

由于Evaluate()被调用了数百万次,因此这种内存分配发生了数百万次。碰巧,CLR在分配内存时在内部执行一些线程间同步 - 否则在多个线程上的分配可能会无意中重叠。将数组从方法本地实例更改为仅分配一次的类实例(但随后在方法本地循环中初始化)消除了可伸缩性问题。

通常,为一个仅在单个方法范围内使用(且有意义)的变量创建类级别成员是反模式。但在这种情况下,由于我需要尽可能最大的可扩展性,我将继续(并记录)这种优化。

后记:在我做了这个改变后,并发进程能够达到1220万次计算/秒。

P.S。感谢Igor Ostrovsky与MSDN博客的密切联系,帮助我识别和诊断问题。

答案 2 :(得分:3)

与顺序算法相比,使用并行算法可以预期非线性缩放,因为并行化存在一些固有的开销。 (理想情况下,当然,你想尽可能接近。)

此外,在顺序算法中通常需要处理一些您不需要的并行算法。除了同步(这可能会让你的工作陷入困境)之外,还有其他一些事情可能发生:

  • CPU和操作系统无法将所有时间用于您的应用程序。因此,它需要不时地进行上下文切换,让其他进程完成一些工作。当您只使用单个核心时,您的流程不太可能被切换出来,因为它有三个其他核心可供选择。请注意,即使您可能认为没有其他任何内容正在运行,操作系统或某些服务仍可能正在执行一些后台工作。
  • 如果每个线程都在访问大量数据,并且这些数据在线程之间不常见,那么很可能无法将所有这些数据存储在CPU缓存中。这意味着需要更多的内存访问,这是(相对)慢。

据我所知,您当前的显式方法在线程之间使用共享迭代器。如果处理在整个阵列中变化很大,这是一个好的解决方案,但是可能存在同步开销以防止跳过元素(检索当前元素并将内部指针移动到下一个元素需要是一个原子操作来防止跳过一个元素。)

因此,对数组进行分区可能是一个更好的主意,假设每个元素的处理时间预计大致相等,而不管元素的位置如何。鉴于您有1000万条记录,这意味着告诉线程1处理元素0到2,499,999,线程2处理元素2,500,000到4,999,999等。您可以为每个线程分配一个ID并使用它来计算实际范围。

另一个小的改进是让主线程作为计算的线程之一。但是,如果我没记错的话,那就是非常小事。

答案 3 :(得分:0)

我当然不会期待线性关系,但我认为你会看到比这更大的收益。我假设所有内核的CPU使用率最大化。我只想了几个想法。

  • 您是否正在使用需要同步的任何共享数据结构(显式或隐式)?
  • 您是否尝试过分析或记录性能计数器以确定瓶颈在哪里?你能不能提供更多线索。

编辑抱歉,我刚注意到您已经解决了我的两点问题。