代码之家  ›  专栏  ›  技术社区  ›  LBushkin

多核机器上.NET操作的非线性缩放

  •  8
  • LBushkin  · 技术社区  · 15 年前

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

    当在多核处理器(IntelCore2 Quad Q6600 2.4GHz)上运行时,随着启动多个线程来处理数据,它显示出非线性伸缩。

    当在单核上作为非多线程循环运行时,该进程每秒能够完成大约240万次计算。当作为四个线程运行时,您期望的吞吐量是四倍——大约每秒900万次计算——但遗憾的是,不是。实际上,它只完成大约410万次。与预期吞吐量相比有点短。

    此外,无论是使用plinq、线程池还是四个显式创建的线程,都会发生这种行为。很奇怪…

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

    我目前的理论:

    1. 所有技术(线程上下文切换等)的开销都超过了计算。
    2. 线程没有被分配到四个核心中的每一个,而是在同一个处理器核心上等待一段时间。不知道如何检验这个理论…
    3. .NET CLR线程未按预期的优先级运行,或者具有一些隐藏的内部开销。

    下面是代码的一个代表性摘录,它应该表现出相同的行为:

        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 );
    
    5 回复  |  直到 15 年前
        1
  •  5
  •   Igor ostrovsky    15 年前

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

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

        2
  •  5
  •   LBushkin    15 年前

    所以我终于弄明白了问题所在——我认为与SO社区分享它会很有用。

    非线性性能的整个问题是 Evaluate() 方法:

    var coordMatrix = new long[100];
    

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

    通常,为仅在单个方法范围内使用(且有意义)的变量创建类级成员是反模式。但在本例中,由于我需要尽可能大的可伸缩性,所以我将使用(并记录)这种优化。

    后记: 在我做了这个更改之后,并发进程能够实现1220万次计算/秒。

    附笔。 伊戈尔·奥斯特罗夫斯基(igor Ostrovsky)因其与msdn博客的日耳曼链接而备受赞誉,这有助于我识别和诊断问题。

        3
  •  3
  •   Michael Madsen    15 年前

    由于并行化中存在一些固有的开销,因此与顺序算法相比,并行算法可以实现非线性缩放。(理想情况下,当然,您希望尽可能接近。)

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

    • CPU和OS不能把所有的时间都花在你的应用上。因此,它需要不时地进行上下文切换,以让其他进程完成一些工作。当您只使用一个核心时,就不太可能关闭进程,因为它还有三个核心可供选择。请注意,即使您可能认为没有其他东西在运行,操作系统或某些服务仍可能在执行一些后台工作。
    • 如果每个线程都在访问大量数据,而这些数据在线程之间并不常见,则很可能无法将所有这些数据存储在CPU缓存中。这意味着需要更多的内存访问,这是(相对)缓慢的。

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

    因此,最好对数组进行分区,假设每个元素的处理时间大致相等,而不管元素的位置如何。假设您有1000万条记录,这意味着告诉线程1处理元素0到249999,线程2处理元素2500000到4999999等。您可以为每个线程分配一个ID,并使用该ID计算实际范围。

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

        4
  •  0
  •   Brian Gideon    15 年前

    我当然不会期望线性关系,但我想你会看到一个更大的收益。我假设所有内核的CPU使用率都已达到最大。我脑子里只有几个想法。

    • 您是否使用任何需要同步的共享数据结构(显式或隐式)?
    • 您是否尝试分析或记录性能计数器以确定瓶颈在哪里?你能再给我一些线索吗?

    编辑: 对不起,我刚注意到你已经解决了我的两个问题。

        5
  •  0
  •   Community Mike Causer    7 年前

    我在这里问了一个类似的问题,题为“为什么在分配大量内存时,我的线程.NET应用程序不能线性缩放?”

    Why doesn't my threaded .Net app scale linearly when allocating large amounts of memory?