簡體   English   中英

為什么處理排序數組比未排序數組慢?

[英]Why is processing a sorted array slower than an unsorted array?

我有一個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);

當我生成我的隨機數組並運行我的搜索100個隨機生成的x值時,搜索在大約四秒內完成。 然而, 知道了排序對搜索巨大奇跡 ,我決定在運行我的100次搜索之前對我的數據進行排序 - 首先是Item1 ,然后是Item2 ,最后是Item3 我期望排序版本由於分支預測而執行得更快一些:我的想法是,一旦我們到達Item1 == x的點,所有進一步檢查t.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)

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

當您對列表進行排序時,您將其按隨機順序排列,因為您的排序鍵是隨機生成的。 這意味着對元組成員的內存訪問是不可預測的。 CPU無法預取內存,幾乎每次訪問元組都是緩存未命中。

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

在這種情況下,緩存未命中的懲罰超過了保存的分支預測懲罰

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

Chris Sinclair在評論中指出, “對於TotalCount大約10,000或更少,排序版本確實執行得更快 ”。 這是因為一個小列表完全適合CPU緩存 內存訪問可能無法預測,但目標始終位於緩存中。 我相信仍有一個小的懲罰,因為即使緩存加載需要一些周期。 但這似乎不是一個問題,因為CPU可以處理多個未完成的負載 ,從而提高吞吐量。 每當CPU命中等待內存時,它仍將在指令流中加速,以盡可能多地排隊內存操作。 此技術用於隱藏延遲。

這種行為表明在現代CPU上預測性能有多難。 從順序存儲器訪問到隨機存儲器訪問時,我們的速度只有2倍 ,這一事實告訴我隱藏內存延遲的情況有多少。 內存訪問可以使CPU停頓50-200個周期。 鑒於第一號可以預期程序在引入隨機存儲器訪問時會變慢> 10倍。

LINQ不知道您的列表是否已排序。

由於具有謂詞參數的Count是所有IEnumerables的擴展方法,我認為它甚至不知道它是否通過有效的隨機訪問在集合上運行。 因此,它只是檢查每個元素, Usr解釋了為什么性能降低了。

要利用排序數組的性能優勢(例如二進制搜索),您將需要進行更多編碼。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM