[英]Non-linear scaling of .NET operations on multi-core machine
我在.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 );
看一下這篇文章: http : //blogs.msdn.com/pfxteam/archive/2008/08/12/8849984.aspx
具體來說,限制並行區域中的內存分配,並仔細檢查寫入以確保它們不會發生在其他線程讀取或寫入的內存位置附近。
所以我終於弄清楚問題是什么 - 我認為與SO社區分享它會很有用。
非線性性能的整個問題是Evaluate()
方法中單行的結果:
var coordMatrix = new long[100];
由於Evaluate()
被調用數百萬次,因此這種內存分配發生了數百萬次。 碰巧,CLR在分配內存時在內部執行一些線程間同步 - 否則在多個線程上的分配可能會無意中重疊。 將數組從方法本地實例更改為僅分配一次的類實例(但隨后在方法本地循環中初始化)消除了可伸縮性問題。
通常,為一個僅在單個方法范圍內使用(且有意義)的變量創建類級別成員是反模式。 但在這種情況下,由於我需要最大可能的可擴展性,我將繼續(並記錄)此優化。
結語:在我做了這個改變之后,並發進程能夠達到1220萬次計算/秒。
PS感謝Igor Ostrovsky與MSDN博客的密切聯系,幫助我識別和診斷問題。
與順序算法相比,使用並行算法可以預期非線性縮放,因為並行化存在一些固有的開銷。 (理想情況下,當然,你想盡可能接近。)
此外,在並行算法中通常需要處理某些事情,而在順序算法中則不需要這些算法。 除了同步(這可能會讓您的工作陷入困境)之外,還有其他一些事情可能發生:
據我所知,您當前的顯式方法使用線程之間的共享迭代器。 如果處理在整個陣列中變化很大,這是一個好的解決方案,但是可能存在同步開銷以防止跳過元素(檢索當前元素並將內部指針移動到下一個元素需要是一個原子操作來防止跳過一個元素)。
因此,對陣列進行分區可能是一個更好的想法,假設每個元素的處理時間預計大致相等,而不管元素的位置如何。 假設您有1000萬條記錄,這意味着告訴線程1處理元素0到2,499,999,線程2處理元素2,500,000到4,999,999等。您可以為每個線程分配一個ID並使用它來計算實際范圍。
另一個小的改進是讓主線程作為計算的線程之一。 但是,如果我沒記錯的話,這是一件非常小的事情。
我當然不會期待線性關系,但我認為你會看到比這更大的收益。 我假設所有內核的CPU使用率最大化。 我只想了幾個想法。
編輯:對不起,我剛注意到你已經解決了我的兩點。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.