เหตุใดการประมวลผลอาร์เรย์ที่เรียงลำดับจึงช้ากว่าอาร์เรย์ที่ไม่เรียงลำดับ


233

ฉันมีรายการTuple<long,long,string>วัตถุที่สร้างขึ้นแบบสุ่มจำนวน 500,000 รายการซึ่งฉันกำลังค้นหา "ระหว่าง" อย่างง่าย:

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

เมื่อฉันสร้างอาเรย์แบบสุ่มของฉันและทำการค้นหาค่าที่สร้างแบบสุ่ม 100 ค่าxการค้นหาจะเสร็จสมบูรณ์ในเวลาประมาณสี่วินาที การรู้ถึงสิ่งมหัศจรรย์ที่ยิ่งใหญ่ที่การเรียงลำดับมีผลต่อการค้นหาแต่ฉันตัดสินใจที่จะเรียงลำดับข้อมูลของฉัน - ก่อนItem1แล้วตามด้วยItem2และสุดท้ายItem3- ก่อนที่จะทำการค้นหา 100 ครั้ง ฉันคาดว่ารุ่นที่เรียงไว้จะทำงานได้เร็วขึ้นเล็กน้อยเนื่องจากการคาดเดาของสาขา: ความคิดของฉันคือเมื่อเราไปถึงจุดที่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)

15
เนื่องจากการคาดคะเนสาขา: p
Soner Gönül

8
@ jalf ฉันคาดว่ารุ่นที่เรียงไว้จะทำงานเร็วขึ้นเล็กน้อยเนื่องจากการคาดการณ์ของสาขา ความคิดของฉันคือเมื่อเราไปถึงจุดที่Item1 == xการตรวจสอบเพิ่มเติมทั้งหมดt.Item1 <= xจะทำนายสาขาอย่างถูกต้องเป็น "ไม่รับ" เร่งส่วนหางของการค้นหา เห็นได้ชัดว่าแนวความคิดนั้นได้รับการพิสูจน์แล้วว่าผิดโดยความจริงอันโหดร้าย :)
dasblinkenlight

1
@ChrisSinclair สังเกตดี! ฉันได้เพิ่มคำอธิบายลงในคำตอบแล้ว
usr

39
คำถามนี้ไม่ซ้ำกับคำถามที่มีอยู่ที่นี่ อย่าลงคะแนนเพื่อปิดเป็นหนึ่ง
ThiefMaster

2
@ Sar009 ไม่เลย! คำถามสองข้อพิจารณาสถานการณ์ที่แตกต่างกันสองอย่างค่อนข้างเป็นธรรมชาติถึงผลลัพธ์ที่แตกต่างกัน
dasblinkenlight

คำตอบ:


269

เมื่อคุณใช้ไม่ได้เรียงลำดับรายการ tuples ทั้งหมดจะถูกเข้าถึงได้ในหน่วยความจำการสั่งซื้อ พวกเขาได้รับการจัดสรรอย่างต่อเนื่องใน RAM ซีพียูรักการเข้าถึงหน่วยความจำตามลำดับเพราะพวกเขาสามารถร้องขอแคชบรรทัดถัดไปได้โดยเฉพาะดังนั้นมันจะปรากฏเมื่อจำเป็น

เมื่อคุณกำลังเรียงลำดับรายการคุณจะใส่ลงในลำดับแบบสุ่มเนื่องจากคีย์การเรียงลำดับของคุณจะถูกสร้างแบบสุ่ม ซึ่งหมายความว่าหน่วยความจำเข้าถึงสมาชิก tuple นั้นไม่แน่นอน CPU ไม่สามารถดึงข้อมูลหน่วยความจำล่วงหน้าได้และเกือบทุกการเข้าถึงสิ่งอันดับเป็นสิ่งที่พลาด

นี่เป็นตัวอย่างที่ดีสำหรับข้อได้เปรียบเฉพาะของการจัดการหน่วยความจำ GC : โครงสร้างข้อมูลที่ได้รับการจัดสรรเข้าด้วยกันและใช้ร่วมกันได้ดีมาก พวกเขามีสถานที่อ้างอิงที่ดี

การลงโทษจากแคชนั้นเกินดุลโทษของการทำนายสาขาในกรณีนี้

ลองเปลี่ยนเป็นstruct-tuple สิ่งนี้จะเรียกคืนประสิทธิภาพเนื่องจากไม่จำเป็นต้องมีตัวชี้ความผิดพลาดเกิดขึ้นขณะใช้งานจริงเพื่อเข้าถึงสมาชิกของ tuple

Chris Sinclair ตั้งข้อสังเกตในความคิดเห็นที่ว่า"สำหรับ TotalCount ประมาณ 10,000 หรือน้อยกว่ารุ่นที่เรียงลำดับจะทำงานได้เร็วขึ้น " เพราะนี่คือรายการเล็ก ๆเหมาะกับทั้งหมดในแคชซีพียู การเข้าถึงหน่วยความจำอาจไม่แน่นอน แต่เป้าหมายอยู่ในแคชเสมอ ฉันเชื่อว่ายังคงมีบทลงโทษเล็กน้อยเพราะแม้แต่ภาระจากแคชก็ใช้เวลาบ้าง แต่นั่นดูเหมือนจะไม่เป็นปัญหาเพราะซีพียูสามารถเล่นหลาย ๆ โหลดที่โดดเด่นซึ่งจะช่วยเพิ่มปริมาณงาน เมื่อใดก็ตามที่ CPU พบการรอหน่วยความจำจะยังคงเพิ่มความเร็วในการสตรีมคำสั่งเพื่อรอคิวการดำเนินการของหน่วยความจำให้มากที่สุดเท่าที่จะทำได้ เทคนิคนี้ใช้เพื่อซ่อนเวลาแฝง

พฤติกรรมประเภทนี้แสดงให้เห็นว่าการทำนายประสิทธิภาพบนซีพียูรุ่นใหม่นั้นยากเพียงใด ความจริงที่ว่าเราช้าลงเพียง 2 เท่าเมื่อเริ่มจากการเข้าถึงหน่วยความจำตามลำดับไปยังหน่วยความจำแบบสุ่มบอกฉันได้ว่าเกิดอะไรขึ้นภายใต้ฝาครอบเพื่อซ่อนเวลาแฝงของหน่วยความจำ การเข้าถึงหน่วยความจำสามารถหยุด CPU สำหรับรอบ 50-200 เนื่องจากโปรแกรมอันดับหนึ่งอาจคาดว่าโปรแกรมจะช้ากว่าถึง 10 เท่าเมื่อแนะนำการเข้าถึงหน่วยความจำแบบสุ่ม


5
เหตุผลที่ดีว่าทำไมทุกสิ่งที่คุณเรียนรู้ใน C / C ++ ไม่ได้ใช้คำต่อคำกับภาษาอย่าง C #!
user541686

37
คุณสามารถยืนยันพฤติกรรมนี้ได้โดยการคัดลอกข้อมูลที่เรียงลำดับลงในnew List<Tuple<long,long,string>>(500000)ทีละรายการก่อนทดสอบรายการใหม่นั้น ในสถานการณ์สมมตินี้การทดสอบแบบเรียงลำดับนั้นเร็วเท่ากับการทดสอบแบบไม่เรียงลำดับซึ่งตรงกับเหตุผลในคำตอบนี้
Bobson

3
ยอดเยี่ยมขอบคุณมาก! ฉันสร้างTuplestruct ที่เทียบเท่ากันและโปรแกรมเริ่มทำงานตามที่ฉันทำนาย: เวอร์ชันที่เรียงนั้นเร็วขึ้นเล็กน้อย ยิ่งกว่านั้นเวอร์ชันที่ไม่เรียงลำดับกลายเป็นสองเท่าเร็ว! ดังนั้นตัวเลขstructที่เรียงกัน 2s ไม่ได้เรียงกับ 1.9s
dasblinkenlight

2
ดังนั้นเราสามารถอนุมานได้ว่าสิ่งนี้ที่แคชพลาดทำให้เจ็บมากกว่าการป้องกันความผิดทางสาขา? ฉันคิดอย่างนั้นและคิดเสมอ ใน C ++ std::vectorเกือบจะทำงานได้ดีกว่าstd::listเสมอ
Nawaz

3
@Mehrdad: ไม่นี่เป็นเรื่องจริงสำหรับ C ++ ด้วย แม้แต่ใน C ++ โครงสร้างข้อมูลขนาดกะทัดรัดก็ยังรวดเร็ว การหลีกเลี่ยงการพลาดแคชนั้นสำคัญใน C ++ เหมือนกับภาษาอื่น ๆ std::vectorvs std::listเป็นตัวอย่างที่ดี
Nawaz

4

LINQ ไม่ทราบว่าคุณเรียงลำดับรายการหรือไม่

เนื่องจาก Count พร้อมพารามิเตอร์เพรดิเคตเป็นวิธีส่วนขยายสำหรับ IEnumerables ทั้งหมดฉันคิดว่ามันไม่ทราบด้วยซ้ำว่ามันทำงานผ่านคอลเลกชันที่มีการเข้าถึงแบบสุ่มอย่างมีประสิทธิภาพหรือไม่ ดังนั้นมันจะตรวจสอบทุกองค์ประกอบและUsrอธิบายว่าทำไมประสิทธิภาพลดลง

หากต้องการใช้ประโยชน์จากประสิทธิภาพการทำงานของอาร์เรย์ที่เรียงลำดับแล้ว (เช่นการค้นหาแบบไบนารี) คุณจะต้องทำการเข้ารหัสเพิ่มเติมอีกเล็กน้อย


5
ฉันคิดว่าคุณเข้าใจผิดคำถาม: แน่นอนผมไม่ได้หวังว่าCountหรือWhereจะ "อย่างใด" รับกับความคิดที่ว่าข้อมูลของฉันจะถูกจัดเรียงและเรียกใช้การค้นหาแบบไบนารีแทนที่จะเป็นธรรมดา "ตรวจสอบทุกอย่าง" ค้นหา ทั้งหมดที่ฉันหวังคือการปรับปรุงบางอย่างเนื่องจากการคาดการณ์ของสาขาที่ดีกว่า (ดูลิงก์ในคำถามของฉัน) แต่เมื่อมันปรากฏออกที่ตั้งของการอ้างอิงสำคัญกว่าสาขาการทำนายครั้งใหญ่
dasblinkenlight
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.