Tại sao xử lý một mảng được sắp xếp chậm hơn một mảng chưa sắp xếp?


233

Tôi có một danh sách 500000 Tuple<long,long,string>đối tượng được tạo ngẫu nhiên mà tôi đang thực hiện tìm kiếm "giữa" đơn giản:

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

Khi tôi tạo mảng ngẫu nhiên của mình và chạy tìm kiếm 100 giá trị được tạo ngẫu nhiên x, các tìm kiếm sẽ hoàn thành trong khoảng bốn giây. Tuy nhiên, biết được những điều kỳ diệu mà việc sắp xếp thực hiện đối với việc tìm kiếm , tôi quyết định sắp xếp dữ liệu của mình - trước tiên Item1, sau đó Item2và cuối cùng Item3- trước khi chạy 100 tìm kiếm của tôi. Tôi dự kiến ​​phiên bản được sắp xếp sẽ thực hiện nhanh hơn một chút vì dự đoán chi nhánh: suy nghĩ của tôi là khi chúng ta đến điểm Item1 == x, tất cả các kiểm tra tiếp theo t.Item1 <= xsẽ dự đoán chính xác chi nhánh là "không mất", tăng tốc phần đuôi của Tìm kiếm. Thật ngạc nhiên, các tìm kiếm mất gấp đôi thời gian trên một mảng được sắp xếp !

Tôi đã thử chuyển đổi thứ tự mà tôi đã chạy thử nghiệm và sử dụng các hạt giống khác nhau cho trình tạo số ngẫu nhiên, nhưng hiệu quả vẫn như nhau: các tìm kiếm trong một mảng chưa được sắp xếp chạy nhanh gần gấp đôi so với các tìm kiếm trong cùng một mảng, nhưng sắp xếp

Có ai có một lời giải thích tốt về hiệu ứng kỳ lạ này? Mã nguồn của các bài kiểm tra của tôi sau đây; Tôi đang sử dụng .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
Vì dự đoán chi nhánh: p
Soner Gönül

8
@jalf Tôi dự kiến ​​phiên bản được sắp xếp sẽ thực hiện nhanh hơn một chút vì dự đoán chi nhánh. Suy nghĩ của tôi là một khi chúng ta đi đến điểm Item1 == x, tất cả các kiểm tra tiếp theo t.Item1 <= xsẽ dự đoán chính xác chi nhánh là "không mất", tăng tốc phần đuôi của tìm kiếm. Rõ ràng, dòng suy nghĩ đó đã được chứng minh là sai bởi thực tế khắc nghiệt :)
dasblinkenlight

1
@ChrisSinclair quan sát tốt! Tôi đã thêm một lời giải thích trong câu trả lời của tôi.
usr

39
Câu hỏi này KHÔNG phải là một bản sao của một câu hỏi hiện có ở đây. Đừng bỏ phiếu để đóng nó làm một.
ThiefMaster

2
@ Sar009 Không hề! Hai câu hỏi xem xét hai kịch bản rất khác nhau, hoàn toàn tự nhiên đi đến kết quả khác nhau.
dasblinkenlight

Câu trả lời:


269

Khi bạn đang sử dụng danh sách chưa sắp xếp, tất cả các bộ dữ liệu được truy cập theo thứ tự bộ nhớ . Chúng đã được phân bổ liên tiếp trong RAM. CPU thích truy cập bộ nhớ theo tuần tự vì chúng có thể yêu cầu dòng bộ đệm tiếp theo một cách suy đoán để nó sẽ luôn có mặt khi cần.

Khi bạn sắp xếp danh sách, bạn sắp xếp nó theo thứ tự ngẫu nhiên vì các khóa sắp xếp của bạn được tạo ngẫu nhiên. Điều này có nghĩa là bộ nhớ truy cập vào các thành viên tuple là không thể đoán trước. CPU không thể tìm nạp trước bộ nhớ và hầu như mọi quyền truy cập vào bộ dữ liệu là lỗi bộ nhớ cache.

Đây là một ví dụ hay cho một lợi thế cụ thể của quản lý bộ nhớ GC : các cấu trúc dữ liệu đã được phân bổ cùng nhau và được sử dụng cùng nhau thực hiện rất độc đáo. Họ có địa phương tuyệt vời của tài liệu tham khảo .

Hình phạt từ bộ nhớ cache vượt xa hình phạt dự đoán chi nhánh đã lưu trong trường hợp này.

Hãy thử chuyển sang một struct-tuple. Điều này sẽ khôi phục hiệu năng vì không có sự phản hồi của con trỏ cần xảy ra trong thời gian chạy để truy cập các thành viên tuple.

Chris Sinclair lưu ý trong các nhận xét rằng "đối với TotalCount khoảng 10.000 hoặc ít hơn, phiên bản được sắp xếp sẽ hoạt động nhanh hơn ". Điều này là do một danh sách nhỏ phù hợp hoàn toàn với bộ đệm CPU . Truy cập bộ nhớ có thể không dự đoán được nhưng mục tiêu luôn ở trong bộ đệm. Tôi tin rằng vẫn còn một hình phạt nhỏ vì ngay cả một tải từ bộ đệm cũng mất một số chu kỳ. Nhưng điều đó dường như không phải là một vấn đề vì CPU có thể xử lý nhiều tải trọng vượt trội , do đó làm tăng thông lượng. Bất cứ khi nào CPU chạm vào bộ nhớ chờ, nó vẫn sẽ tăng tốc trong luồng lệnh để xếp hàng nhiều hoạt động bộ nhớ nhất có thể. Kỹ thuật này được sử dụng để che giấu độ trễ.

Kiểu hành vi này cho thấy mức độ khó để dự đoán hiệu năng trên các CPU hiện đại. Thực tế là chúng ta chỉ chậm hơn 2 lần khi chuyển từ truy cập bộ nhớ ngẫu nhiên sang bộ nhớ ngẫu nhiên cho tôi biết có bao nhiêu điều đang diễn ra dưới vỏ bọc để che giấu độ trễ của bộ nhớ. Một truy cập bộ nhớ có thể trì hoãn CPU trong 50-200 chu kỳ. Cho rằng số một có thể mong đợi chương trình trở nên chậm hơn> 10 lần khi giới thiệu truy cập bộ nhớ ngẫu nhiên.


5
Lý do chính đáng tại sao mọi thứ bạn học trong C / C ++ không áp dụng nguyên văn cho một ngôn ngữ như C #!
dùng541686

37
Bạn có thể xác nhận hành vi này bằng cách sao chép thủ công dữ liệu đã sắp xếp vào new List<Tuple<long,long,string>>(500000)từng cái một trước khi kiểm tra danh sách mới đó. Trong kịch bản này, bài kiểm tra được sắp xếp cũng nhanh như bài kiểm tra chưa được sắp xếp, phù hợp với lý do của câu trả lời này.
Bobson

3
Tuyệt vời! Cảm ơn bạn rất nhiều! Tôi đã tạo một Tuplecấu trúc tương đương và chương trình bắt đầu hành xử theo cách tôi dự đoán: phiên bản được sắp xếp nhanh hơn một chút. Hơn nữa, phiên bản chưa sắp xếp trở nên nhanh gấp đôi! Vì vậy, các số có struct2 giây chưa được sắp xếp so với 1.9 được sắp xếp.
dasblinkenlight

2
Vì vậy, chúng ta có thể suy luận từ điều này rằng bộ nhớ cache gây tổn thương nhiều hơn so với việc hiểu sai chi nhánh? Tôi nghĩ vậy, và luôn nghĩ như vậy. Trong C ++, std::vectorhầu như luôn luôn hoạt động tốt hơn std::list.
Nawaz

3
@Mehrdad: Không. Điều này cũng đúng với C ++. Ngay cả trong C ++, cấu trúc dữ liệu nhỏ gọn cũng nhanh. Tránh bỏ lỡ bộ nhớ cache cũng quan trọng trong C ++ như trong bất kỳ ngôn ngữ nào khác. std::vectorvs std::listlà một ví dụ tốt.
Nawaz

4

LINQ không biết liệu danh sách của bạn có được sắp xếp hay không.

Vì Count với tham số vị ngữ là phương thức mở rộng cho tất cả các IEnumerables, tôi nghĩ rằng nó thậm chí không biết liệu nó có chạy trên bộ sưu tập với truy cập ngẫu nhiên hiệu quả hay không. Vì vậy, nó chỉ đơn giản là kiểm tra mọi yếu tố và Usr giải thích lý do tại sao hiệu suất thấp hơn.

Để khai thác lợi ích hiệu suất của mảng được sắp xếp (như tìm kiếm nhị phân), bạn sẽ phải thực hiện thêm một chút mã hóa.


5
Tôi nghĩ rằng bạn đã hiểu nhầm câu hỏi: tất nhiên tôi không hy vọng rằng Counthoặc Where"bằng cách nào đó" sẽ biết được rằng dữ liệu của tôi được sắp xếp và chạy tìm kiếm nhị phân thay vì tìm kiếm "kiểm tra mọi thứ" đơn giản. Tất cả những gì tôi đã hy vọng là một số cải tiến do dự đoán chi nhánh tốt hơn (xem liên kết bên trong câu hỏi của tôi), nhưng khi nó bật ra, địa phương tham chiếu dự đoán chi nhánh thời gian lớn.
dasblinkenlight
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.