我在 .NET 应用程序中遇到了一个奇怪的行为,该应用程序对一组内存数据执行一些高度并行处理。
当在多核处理器(IntelCore2 Quad Q6600 2.4GHz)上运行时,它表现出非线性缩放,因为启动了多个线程来处理数据。
当在单核上作为非多线程循环运行时,该进程每秒能够完成大约 240 万次计算。当作为四个线程运行时,您会期望吞吐量是原来的四倍——大约每秒 900 万次计算——但很可惜,没有。在实践中,它每秒只能完成大约 410 万次……与预期的吞吐量相比,还差很多。
此外,无论我使用 PLINQ、线程池还是四个显式创建的线程,都会发生这种行为。很奇怪...
机器上没有使用 CPU 时间运行其他任何东西,计算中也没有任何锁或其他同步对象......它应该只是提前撕开数据。我已经通过在进程运行时查看 perfmon 数据确认了这一点(尽可能)......并且没有报告线程争用或垃圾收集活动。
我目前的理论:
- 所有技术(线程上下文切换等)的开销使计算不堪重负
- 线程没有被分配给四个核心中的每一个,而是花一些时间在同一个处理器核心上等待......不知道如何测试这个理论......
- .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 );