2012-12-24 50 views
222

我有500000名隨機生成Tuple<long,long,string>對象列表上我執行一個簡單的搜索「之間」時:爲什麼處理排序後的數組要比未排序的數組慢?

var data = new List<Tuple<long,long,string>>(500000); 
... 
var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x); 

當我生成我隨機陣列和運行我爲x 100個隨機生成的值的搜索,所述大約四秒鐘完成搜索。然而,在知道great wonders that sorting does to searching後,我決定對我的數據進行排序 - 首先按Item1,然後按Item2,最後按Item3 - 在運行我的100次搜索之前。由於分支預測,我預計排序後的版本會執行得更快一些:我的想法是,一旦我們達到Item1 == x的所有進一步檢查將正確預測分支爲「不帶」,加快尾部的搜索。令我驚訝的是,搜索花費了兩倍於排序陣列

我試着開關周圍,我跑我的實驗,並用不同的種子隨機數發生器的順序,但效果一直不變:在排序的數組的搜索跑了近兩倍的速度在搜查相同的數組,但排序!

有沒有人有這個奇怪的效果很好的解釋?我的測試源代碼如下;我正在使用.NET 4.0。


private const int TotalCount = 500000; 
private const int TotalQueries = 100; 
private static long NextLong(Random r) { 
    var data = new byte[8]; 
    r.NextBytes(data); 
    return BitConverter.ToInt64(data, 0); 
} 
private class TupleComparer : IComparer<Tuple<long,long,string>> { 
    public int Compare(Tuple<long,long,string> x, Tuple<long,long,string> y) { 
     var res = x.Item1.CompareTo(y.Item1); 
     if (res != 0) return res; 
     res = x.Item2.CompareTo(y.Item2); 
     return (res != 0) ? res : String.CompareOrdinal(x.Item3, y.Item3); 
    } 
} 
static void Test(bool doSort) { 
    var data = new List<Tuple<long,long,string>>(TotalCount); 
    var random = new Random(1000000007); 
    var sw = new Stopwatch(); 
    sw.Start(); 
    for (var i = 0 ; i != TotalCount ; i++) { 
     var a = NextLong(random); 
     var b = NextLong(random); 
     if (a > b) { 
      var tmp = a; 
      a = b; 
      b = tmp; 
     } 
     var s = string.Format("{0}-{1}", a, b); 
     data.Add(Tuple.Create(a, b, s)); 
    } 
    sw.Stop(); 
    if (doSort) { 
     data.Sort(new TupleComparer()); 
    } 
    Console.WriteLine("Populated in {0}", sw.Elapsed); 
    sw.Reset(); 
    var total = 0L; 
    sw.Start(); 
    for (var i = 0 ; i != TotalQueries ; i++) { 
     var x = NextLong(random); 
     var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x); 
     total += cnt; 
    } 
    sw.Stop(); 
    Console.WriteLine("Found {0} matches in {1} ({2})", total, sw.Elapsed, doSort ? "Sorted" : "Unsorted"); 
} 
static void Main() { 
    Test(false); 
    Test(true); 
    Test(false); 
    Test(true); 
} 

Populated in 00:00:01.3176257 
Found 15614281 matches in 00:00:04.2463478 (Unsorted) 
Populated in 00:00:01.3345087 
Found 15614281 matches in 00:00:08.5393730 (Sorted) 
Populated in 00:00:01.3665681 
Found 15614281 matches in 00:00:04.1796578 (Unsorted) 
Populated in 00:00:01.3326378 
Found 15614281 matches in 00:00:08.6027886 (Sorted) 
+14

由於分支預測的:P –

+8

@jalf我預期排序版本來執行,因爲分支預測的快一點。我的想法是,一旦我們到達了'Item1 == x'的位置,'t.Item1 <= x'的所有進一步檢查都會正確地預測分支爲「不帶」,從而加快了搜索的尾部。顯然,這種思維線已經被證明是錯誤的殘酷的現實:) – dasblinkenlight

+0

有趣的是,'TotalCount'左右'10,000'以下,排序的版本並執行得更快(當然,平凡更快那些小數字)(僅供參考,你的代碼可能希望具有'var data List = new List >(500000)'的初始大小'而不是硬編碼容量''TotalCount'' –

回答

257

當您使用未排序列表中的所有元組在內存階訪問。它們已經在RAM中連續分配。 CPU喜歡順序訪問內存,因爲它們可以推測性地請求下一個緩存行,以便在需要時始終存在。

當你整理列表時,你把它放入隨機順序因爲你的排序鍵是隨機生成的。這意味着訪問元組成員的內存是不可預知的。 CPU不能預取內存,幾乎每個對元組的訪問都是緩存未命中。

這是GC內存管理特定優勢的一個很好的例子:已經分配在一起並且一起使用的數據結構表現得非常好。他們有很好的地點參考

從緩存中的點球偏出勝過在這種情況下,保存的分支預測點球

嘗試切換到struct -tuple。這將恢復性能,因爲在運行時不需要使用指針取消引用來訪問元組成員。

Chris Sinclair在評論中指出「對於TotalCount大約10,000或更少,排序後的版本確實執行得更快」。這是因爲一個小列表完全適合CPU高速緩存。內存訪問可能是不可預測的,但目標總是在緩存中。我相信還是有一點小小的損失,因爲即使是來自緩存的負載也需要一些週期。但這似乎不成問題,因爲CPU可以處理多個未完成的負載,從而提高吞吐量。無論CPU何時等待內存,它都會在指令流中加速前進,以儘可能地排隊儘可能多的內存操作。該技術用於隱藏延遲。

這種行爲表明它是如何很難預測在現代CPU的性能。事實上,我們是只有兩倍慢從順序訪問隨機內存訪問告訴我有多少事情在封面下隱藏內存延遲。內存訪問可能會使CPU停止50-200個週期。在引入隨機存儲器訪問的情況下,預計該程序的速度會降低10倍以上。

+5

很好的理由爲什麼在C/C++中學習的所有東西都不會逐字地應用於像C#這樣的語言! – Mehrdad

+33

在測試新列表之前,您可以通過手動將已排序的數據複製到「新List >(500000)」來確認此行爲。在這種情況下,排序的測試和未排序的測試一樣快,這與此答案的推理相匹配。 – Bobson

+3

非常好,非常感謝!我做了一個等效的'Tuple'結構,程序開始按照我預測的方式運行:排序的版本更快一些。而且,未分類的版本變得快了一倍!所以具有'struct'的數字是2s未排序與1.9s排序。 – dasblinkenlight

3

LINQ不知道你是否列表進行排序或沒有。

由於伯爵與謂詞參數是所有IEnumerables擴展方法,我認爲它甚至不知道它是否運行在高效隨機存取集合。所以,它只是檢查每個元素,並解釋爲什麼性能降低。

要利用有序陣列(例如二進制搜索)的性能優勢,你就必須做一點點的編碼。

+5

我認爲你誤解了這個問題:當然,我並不是希望Count或者Where可以「以某種方式」接受我的數據被排序的想法,並且運行二進制搜索而不是簡單的「檢查所有內容「搜索。我所希望的是由於更好的分支預測(請參閱我的問題中的鏈接)而得到一些改進,但事實證明,參考的局部性勝過分支預測的大時間。 – dasblinkenlight