我在.NET应用程序中遇到一种奇怪的行为,它对一组内存数据执行一些高度并行的处理。
在多核处理器(IntelCore2 Quad Q6600 2.4GHz)上运行时,由于启动了多个线程来处理数据,因此它呈现出非线性缩放。
当作为单核上的非多线程循环运行时,该进程每秒能够完成大约240万次计算。当作为四个线程运行时,您可以预期吞吐量的四倍 - 在每秒900万次计算的某个地方 - 但是,唉,没有。在实践中,它每秒仅完成约4.1百万......与预期的吞吐量相当短。
此外,无论我使用PLINQ,线程池还是四个显式创建的线程,都会发生这种情况。很奇怪......
使用CPU时间在机器上没有其他任何东西在运行,计算中也没有涉及任何锁或其他同步对象...它应该直接通过数据。我已经通过在进程运行时查看perfmon数据来确认这一点(尽可能)......并且没有报告的线程争用或垃圾收集活动。
我的理论:
以下是应该表现出相同行为的代码摘录:
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 );
答案 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)
与顺序算法相比,使用并行算法可以预期非线性缩放,因为并行化存在一些固有的开销。 (理想情况下,当然,你想尽可能接近。)
此外,在顺序算法中通常需要处理一些您不需要的并行算法。除了同步(这可能会让你的工作陷入困境)之外,还有其他一些事情可能发生:
据我所知,您当前的显式方法在线程之间使用共享迭代器。如果处理在整个阵列中变化很大,这是一个好的解决方案,但是可能存在同步开销以防止跳过元素(检索当前元素并将内部指针移动到下一个元素需要是一个原子操作来防止跳过一个元素。)
因此,对数组进行分区可能是一个更好的主意,假设每个元素的处理时间预计大致相等,而不管元素的位置如何。鉴于您有1000万条记录,这意味着告诉线程1处理元素0到2,499,999,线程2处理元素2,500,000到4,999,999等。您可以为每个线程分配一个ID并使用它来计算实际范围。
另一个小的改进是让主线程作为计算的线程之一。但是,如果我没记错的话,那就是非常小事。
答案 3 :(得分:0)
我当然不会期待线性关系,但我认为你会看到比这更大的收益。我假设所有内核的CPU使用率最大化。我只想了几个想法。
编辑抱歉,我刚注意到您已经解决了我的两点问题。