Tại sao việc xử lý một mảng được sắp xếp * chậm hơn * so với một mảng không được sắp xếp? (ArrayList.indexOf của Java)


80

Tiêu đề liên quan đến Tại sao xử lý mảng được sắp xếp nhanh hơn mảng không được sắp xếp?

Đây cũng là một hiệu ứng dự đoán nhánh phải không? Hãy lưu ý: ở đây quá trình xử lý cho mảng đã sắp xếp chậm hơn !!

Hãy xem xét đoạn mã sau:

private static final int LIST_LENGTH = 1000 * 1000;
private static final long SLOW_ITERATION_MILLIS = 1000L * 10L;

@Test
public void testBinarySearch() {
    Random r = new Random(0);
    List<Double> list = new ArrayList<>(LIST_LENGTH);
    for (int i = 0; i < LIST_LENGTH; i++) {
        list.add(r.nextDouble());
    }
    //Collections.sort(list);
    // remove possible artifacts due to the sorting call
    // and rebuild the list from scratch:
    list = new ArrayList<>(list);

    int nIterations = 0;
    long startTime = System.currentTimeMillis();
    do {
        int index = r.nextInt(LIST_LENGTH);
        assertEquals(index, list.indexOf(list.get(index)));
        nIterations++;
    } while (System.currentTimeMillis() < startTime + SLOW_ITERATION_MILLIS);
    long duration = System.currentTimeMillis() - startTime;
    double slowFindsPerSec = (double) nIterations / duration * 1000;
    System.out.println(slowFindsPerSec);

    ...
}

Điều này in ra giá trị khoảng 720 trên máy của tôi.

Bây giờ nếu tôi kích hoạt lệnh gọi sắp xếp bộ sưu tập, giá trị đó sẽ giảm xuống 142. Tại sao?!?

Kết quả kết luận, chúng không thay đổi nếu tôi tăng số lần lặp / lần.

Phiên bản Java là 1.8.0_71 (Oracle VM, 64 bit), chạy trên Windows 10, thử nghiệm JUnit trong Eclipse Mars.

CẬP NHẬT

Có vẻ như liên quan đến truy cập bộ nhớ liền kề (Đối tượng kép được truy cập theo thứ tự tuần tự so với thứ tự ngẫu nhiên). Hiệu ứng bắt đầu biến mất đối với tôi đối với độ dài mảng khoảng 10k trở xuống.

Cảm ơn assylias đã cung cấp kết quả :

/**
 * Benchmark                     Mode  Cnt  Score   Error  Units
 * SO35018999.shuffled           avgt   10  8.895 ± 1.534  ms/op
 * SO35018999.sorted             avgt   10  8.093 ± 3.093  ms/op
 * SO35018999.sorted_contiguous  avgt   10  1.665 ± 0.397  ms/op
 * SO35018999.unsorted           avgt   10  2.700 ± 0.302  ms/op
 */



3
Thực hiện lại các phép đo của bạn với một khung điểm chuẩn thích hợp như JMH nếu bạn muốn có kết quả có ý nghĩa.
Clashsoft

7
Ngoài ra, ngay cả khi không có JMH, phương pháp thử nghiệm của bạn vẫn còn thiếu sót về mặt khái niệm. Bạn đang thử nghiệm tất cả mọi thứ, bao gồm cả RNG System.currentTimeMillisassertEquals. Không có lặp lại khởi động, không có lặp lại nói chung, bạn dựa vào lượng thời gian cố định và kiểm tra xem đã thực hiện được bao nhiêu trong thời gian đó. Xin lỗi, nhưng thử nghiệm này thực sự vô dụng.
Clashsoft

4
Nhận được kết quả tương tự với JMH ...
assylias

Câu trả lời:


88

Nó trông giống như hiệu ứng bộ nhớ đệm / tìm nạp trước.

Manh mối là bạn so sánh Nhân đôi (đối tượng), không phải nhân đôi (nguyên thủy). Khi bạn cấp phát các đối tượng trong một luồng, chúng thường được cấp phát tuần tự trong bộ nhớ. Vì vậy, khi indexOfquét một danh sách, nó sẽ đi qua các địa chỉ bộ nhớ tuần tự. Điều này tốt cho quá trình tìm nạp trước bộ nhớ cache của CPU.

Nhưng sau khi bạn sắp xếp danh sách, bạn vẫn phải thực hiện cùng một số lần tra cứu bộ nhớ trung bình, nhưng lần này việc truy cập bộ nhớ sẽ theo thứ tự ngẫu nhiên.

CẬP NHẬT

Đây là điểm chuẩn để chứng minh rằng thứ tự của các đối tượng được phân bổ là quan trọng.

Benchmark            (generator)  (length)  (postprocess)  Mode  Cnt  Score   Error  Units
ListIndexOf.indexOf       random   1000000           none  avgt   10  1,243 ± 0,031  ms/op
ListIndexOf.indexOf       random   1000000           sort  avgt   10  6,496 ± 0,456  ms/op
ListIndexOf.indexOf       random   1000000        shuffle  avgt   10  6,485 ± 0,412  ms/op
ListIndexOf.indexOf   sequential   1000000           none  avgt   10  1,249 ± 0,053  ms/op
ListIndexOf.indexOf   sequential   1000000           sort  avgt   10  1,247 ± 0,037  ms/op
ListIndexOf.indexOf   sequential   1000000        shuffle  avgt   10  6,579 ± 0,448  ms/op

2
Nếu điều này là đúng, xáo trộn thay vì phân loại nên tạo ra kết quả tương tự
David Soroko

1
@DavidSoroko thì có.
assylias

1
@DavidSoroko Kết quả điểm chuẩn đầy đủ với chưa phân loại, xáo trộn, sắp xếp và sắp xếp liền kề ở cuối mã điểm chuẩn .
assylias

1
@assylias Một tiện ích mở rộng thú vị cũng có thể là tạo các số tuần tự (và đăng mã kết quả ở đây sẽ khiến câu trả lời của tôi trở nên lỗi thời).
Marco 13

1
Chỉ cần nhấn mạnh, trong list.indexOf(list.get(index))những list.get(index)không có lợi bằng mọi cách từ tìm nạp trước từ indexlà ngẫu nhiên. Giá của list.get(index)là như nhau bất kể thời tiết, danh sách có được sắp xếp hay không. Tìm nạp trước đá trong chỉ cholist.indexOf()
David Soroko

25

Tôi nghĩ rằng chúng ta đang thấy ảnh hưởng của việc bỏ sót bộ nhớ cache:

Khi bạn tạo danh sách chưa được sắp xếp

for (int i = 0; i < LIST_LENGTH; i++) {
    list.add(r.nextDouble());
}

tất cả các bộ đôi rất có thể được phân bổ trong một vùng bộ nhớ liền kề. Lặp đi lặp lại điều này sẽ tạo ra một vài lần bỏ sót bộ nhớ cache.

Mặt khác trong danh sách đã sắp xếp, các tham chiếu trỏ tới bộ nhớ một cách hỗn loạn.

Bây giờ nếu bạn tạo một danh sách được sắp xếp với bộ nhớ liền kề:

Collection.sort(list);
List<Double> list2 = new ArrayList<>();
for (int i = 0; i < LIST_LENGTH; i++) {
    list2.add(new Double(list.get(i).doubleValue()));
}

danh sách được sắp xếp này có cùng hiệu suất so với danh sách ban đầu (thời gian của tôi).


8

Như một ví dụ đơn giản xác nhận câu trả lời của werocâu trả lời của apangin (+1!): Sau đây là một so sánh đơn giản của cả hai tùy chọn:

  • Tạo các số ngẫu nhiên và sắp xếp chúng theo tùy chọn
  • Tạo các số tuần tự và trộn chúng tùy ý

Nó cũng không được thực hiện như một điểm chuẩn JMH, nhưng tương tự như mã gốc, chỉ với những sửa đổi nhỏ để quan sát hiệu ứng:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

public class SortedListTest
{
    private static final long SLOW_ITERATION_MILLIS = 1000L * 3L;

    public static void main(String[] args)
    {
        int size = 100000;
        testBinarySearchOriginal(size, true);
        testBinarySearchOriginal(size, false);
        testBinarySearchShuffled(size, true);
        testBinarySearchShuffled(size, false);
    }

    public static void testBinarySearchOriginal(int size, boolean sort)
    {
        Random r = new Random(0);
        List<Double> list = new ArrayList<>(size);
        for (int i = 0; i < size; i++)
        {
            list.add(r.nextDouble());
        }
        if (sort)
        {
            Collections.sort(list);
        }
        list = new ArrayList<>(list);

        int count = 0;
        int nIterations = 0;
        long startTime = System.currentTimeMillis();
        do
        {
            int index = r.nextInt(size);
            if (index == list.indexOf(list.get(index)))
            {
                count++;
            }
            nIterations++;
        }
        while (System.currentTimeMillis() < startTime + SLOW_ITERATION_MILLIS);
        long duration = System.currentTimeMillis() - startTime;
        double slowFindsPerSec = (double) nIterations / duration * 1000;

        System.out.printf("Size %8d sort %5s iterations %10.3f count %10d\n",
            size, sort, slowFindsPerSec, count);
    }

    public static void testBinarySearchShuffled(int size, boolean sort)
    {
        Random r = new Random(0);
        List<Double> list = new ArrayList<>(size);
        for (int i = 0; i < size; i++)
        {
            list.add((double) i / size);
        }
        if (!sort)
        {
            Collections.shuffle(list);
        }
        list = new ArrayList<>(list);

        int count = 0;
        int nIterations = 0;
        long startTime = System.currentTimeMillis();
        do
        {
            int index = r.nextInt(size);
            if (index == list.indexOf(list.get(index)))
            {
                count++;
            }
            nIterations++;
        }
        while (System.currentTimeMillis() < startTime + SLOW_ITERATION_MILLIS);
        long duration = System.currentTimeMillis() - startTime;
        double slowFindsPerSec = (double) nIterations / duration * 1000;

        System.out.printf("Size %8d sort %5s iterations %10.3f count %10d\n",
            size, sort, slowFindsPerSec, count);
    }

}

Đầu ra trên máy của tôi là

Size   100000 sort  true iterations   8560,333 count      25681
Size   100000 sort false iterations  19358,667 count      58076
Size   100000 sort  true iterations  18554,000 count      55662
Size   100000 sort false iterations   8845,333 count      26536

cho thấy một cách độc đáo rằng thời gian hoàn toàn đối lập với thời gian khác: Nếu các số ngẫu nhiên được sắp xếp, thì phiên bản được sắp xếp sẽ chậm hơn. Nếu các số tuần tự bị xáo trộn, thì phiên bản trộn sẽ chậm hơn.

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.