8

我在 .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 );
4

4 回答 4

5

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

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

于 2009-09-20T19:03:00.067 回答
5

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

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

var coordMatrix = new long[100];

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

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

结语:我做了这个改动后,并发进程可以达到1220万次/秒。

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

于 2009-09-21T03:10:23.200 回答
3

与顺序算法相比,并行算法预计会出现非线性缩放,因为并行化存在一些固有开销。(当然,理想情况下,您希望尽可能靠近。)

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

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

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

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

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

于 2009-09-20T00:58:33.747 回答
0

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

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

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

于 2009-09-20T00:31:32.913 回答