Sıralı bir diziyi işlemek neden sıralanmamış bir diziden daha yavaş işleniyor?


233

Tuple<long,long,string>Üzerinde basit bir "arasında" arama gerçekleştiriyorum 500000 rasgele oluşturulan nesnelerin bir listesi var :

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

Rastgele dizimi oluşturduğumda ve aramamı rastgele oluşturulmuş 100 değer için çalıştırdığımda x, aramalar yaklaşık dört saniye içinde tamamlanıyor. Bununla birlikte, sıralamanın aramaya yaptığı harika harikaları bilerek, 100 aramamı çalıştırmadan önce verilerimi - önce Item1, sonra Item2ve son olarak Item3- sıralamaya karar verdim . Sıralı versiyonun şube tahmini nedeniyle biraz daha hızlı performans göstermesini bekledim: düşüncem, bir kez daha geldiğimizde Item1 == x, tüm diğer kontrollerin t.Item1 <= xdalı "kuyruk alma" olarak doğru şekilde tahmin edeceği ve kuyruk kısmını hızlandıracağıydı. arama. Çok şaşırdım, aramalar sıralı bir dizide iki kat daha uzun sürdü !

Deneylerimi çalıştırdığım sırada geçiş yapmayı denedim ve rastgele sayı üreteci için farklı tohum kullandım, ancak etki aynı oldu: sıralanmamış bir dizideki aramalar, aynı dizideki aramaların neredeyse iki katı kadar hızlı koştu, ancak sıralanmış!

Herkes bu garip etki hakkında iyi bir açıklama var mı? Testlerimin kaynak kodu şöyle; .NET 4.0 kullanıyorum.


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)

15
Şube tahmini nedeniyle: p
Soner Gönül

8
@jalf Sıralanan sürümün, şube tahmini nedeniyle biraz daha hızlı çalışmasını bekledim. Benim düşüncem, bir kez daha yaptığımız Item1 == xtüm kontrollerin t.Item1 <= xdalın "yok alma" olarak doğru tahmin edileceği ve aramanın kuyruk kısmını hızlandıracağı noktaya geldiğiydi. Açıkçası, bu düşünce çizgisi sert gerçeklik tarafından yanlış kanıtlanmıştır :)
dasblinkenlight

1
@ChrisSinclair iyi gözlem! Cevabıma bir açıklama ekledim.
usr

39
Bu soru, burada var olan bir sorunun kopyası DEĞİLDİR . Birini kapatmak için oy vermeyin.
ThiefMaster

2
@ Sar009 Hiç de değil! İki soru, doğal olarak farklı sonuçlara ulaşan iki çok farklı senaryoyu ele alıyor.
dasblinkenlight

Yanıtlar:


269

Sıralanmamış listeyi kullanırken tüm gruplara bellek sırasıyla erişilir . RAM'a ardışık olarak tahsis edilmişlerdir. İşlemciler, bir sonraki önbellek hattını spekülatif olarak talep edebildikleri için belleğe sırayla erişmeyi severler, böylece gerektiğinde her zaman mevcut olurlar.

Listeyi sıralarken, sıralama anahtarlarınız rastgele oluşturulduğundan listeyi rastgele sıraya koyarsınız . Bu, demet üyelerine bellek erişiminin öngörülemez olduğu anlamına gelir. CPU belleği önceden getiremiyor ve bir tuple'a neredeyse her erişim bir önbellek kaçırıyor.

Bu, GC bellek yönetiminin özel bir avantajı için güzel bir örnektir : birlikte tahsis edilen ve birlikte kullanılan veri yapıları çok iyi performans gösterir. Onlar büyük bir referans yerellik var .

Önbellek hatalarının cezası , bu durumda kaydedilen şube tahmin cezasından ağır basar .

struct-Tuple'e geçmeyi deneyin . Tuple üyelerine erişmek için çalışma zamanında işaretçi gerektirmemesi gerektiğinden bu performansı geri yükler.

Chris Sinclair, "TotalCount için yaklaşık 10,000 veya daha az, sıralanan sürümün daha hızlı performans gösterdiğini " belirtti. Bunun nedeni, küçük bir listenin tamamen CPU önbelleğine sığmasıdır . Bellek erişimi tahmin edilemeyebilir, ancak hedef her zaman önbellektedir. Hala küçük bir ceza olduğuna inanıyorum çünkü önbellekten bir yük bile bazı döngüler alıyor. Ancak bu bir sorun gibi görünmüyor çünkü CPU birden fazla olağanüstü yükü dengeleyebilir , böylece verimi artırabilir. CPU bellek beklemesine rağmen, olabildiğince çok bellek işlemini kuyruğa almak için yönerge akışında ilerlemeye devam edecektir. Bu teknik gecikmeyi gizlemek için kullanılır.

Bu tür davranışlar modern CPU'lardaki performansı tahmin etmenin ne kadar zor olduğunu gösterir. Sıralıdan rasgele bellek erişimine geçerken sadece 2 kat daha yavaş olduğumuz gerçeği bana, bellek gecikmesini gizlemek için kapakların altında ne kadar olduğunu gösteriyor. Bellek erişimi CPU'yu 50-200 döngü boyunca durdurabilir. Bu bir numara göz önüne alındığında, rastgele bellek erişimleri verilirken programın> 10 kat daha yavaş olmasını bekleyebilir.


5
C / C ++ 'da öğrendiğiniz her şeyin C # gibi bir dile aynen uygulamamasının iyi bir nedeni!
user541686

37
new List<Tuple<long,long,string>>(500000)Yeni listeyi test etmeden önce , sıralanan verileri tek tek el ile kopyalayarak bu davranışı onaylayabilirsiniz . Bu senaryoda, sıralı sınama, sıralanmamış sınama kadar hızlıdır ve bu da bu yanıtın gerekçesiyle eşleşir.
Bobson

3
Mükemmel Çok teşekkür ederim! Eşdeğer bir Tupleyapı yaptım ve program tahmin ettiğim şekilde davranmaya başladı: sıralanan sürüm biraz daha hızlıydı. Ayrıca, sıralanmamış sürüm iki kat daha hızlı oldu! Yani sayıları struct2s sıralanmamış ve 1,9s sıralanır.
dasblinkenlight

2
Öyleyse, önbellek özleminin dal yanlış tahmininden daha fazla acıttığını söyleyebilir miyiz? Ben öyle düşünüyorum ve hep böyle düşündüm. C ++ 'da std::vectorneredeyse her zaman daha iyi performans gösterir std::list.
Nawaz

3
@Mehrdad: Hayır. Bu C ++ için de geçerlidir. C ++ 'da bile, kompakt veri yapıları hızlıdır. Önbellek kaybından kaçınmak C ++ 'da diğer dillerde olduğu kadar önemlidir. std::vectorvs std::listiyi bir örnektir.
Nawaz

4

LINQ listenin sıralanıp sıralanmadığını bilmez.

Yüklem parametresi ile Count tüm IEnumerables için uzantısı yöntemi olduğundan, verimli rasgele erişim ile koleksiyon üzerinde çalışıyor olup olmadığını bile bilmiyorum düşünüyorum. Böylece, her öğeyi kontrol eder ve Usr performansın neden düştüğünü açıklar.

Sıralanmış dizinin performans avantajlarından yararlanmak için (ikili arama gibi), biraz daha kodlama yapmanız gerekir.


5
Soruyu yanlış anladığınızı düşünüyorum: elbette bunu ummuyordum Countya Whereda verilerimin sıralandığı fikrini "bir şekilde" alıp düz bir "her şeyi kontrol et" araması yerine ikili bir arama yapmayı umuyordum . Tek umduğum, daha iyi şube tahmini (sorumun içindeki bağlantıya bakınız) nedeniyle bazı iyileştirmelerdi, ancak ortaya çıktıkça, referansın yeri şube tahminini büyük zamanla gölgede bıraktı.
dasblinkenlight
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.