정렬되지 않은 배열보다 정렬 된 배열을 * 느리게 * 처리하는 이유는 무엇입니까? (자바의 ArrayList.indexOf)


80

제목은 왜 정렬되지 않은 배열보다 정렬 된 배열을 처리하는 것이 더 빠릅니까?

이것도 분기 예측 효과입니까? 주의 : 여기 정렬 된 배열의 처리 속도느립니다 !!

다음 코드를 고려하십시오.

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);

    ...
}

이것은 내 컴퓨터에서 약 720의 값을 인쇄합니다.

이제 컬렉션 정렬 호출을 활성화하면 그 값이 142로 떨어집니다. 왜?!?

결과 결정적이며 반복 횟수 / 시간을 늘려도 변경되지 않습니다.

Java 버전은 1.8.0_71 (Oracle VM, 64 비트)이며 Windows 10, Eclipse Mars의 JUnit 테스트에서 실행됩니다.

최신 정보

연속적인 메모리 액세스와 관련이있는 것 같습니다 (순차적으로 액세스되는 이중 객체와 임의의 순서로 액세스 됨). 약 10k 이하의 어레이 길이에서 효과가 사라지기 시작합니다.

결과 를 제공 assylias에게 감사드립니다 .

/**
 * 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
의미있는 결과를 원한다면 JMH 와 같은 적절한 벤치마킹 프레임 워크로 측정을 다시 수행하십시오 .
Clashsoft

7
또한 JMH가 없어도 테스트 방법은 개념적으로 결함이 있습니다. RNG System.currentTimeMillisassertEquals. 워밍업 반복이없고 일반적으로 반복이 없으며 일정한 시간에 의존하고 그 시간에 얼마나 많이 수행되었는지 확인합니다. 죄송합니다.이 테스트는 사실상 쓸모가 없습니다.
Clashsoft

4
... JMH와 유사한 결과를 얻기
assylias

답변:


88

캐싱 / 프리 페치 효과처럼 보입니다.

단서는 double (원시)이 아닌 Double (객체)을 비교한다는 것입니다. 한 스레드에 개체를 할당하면 일반적으로 메모리에 순차적으로 할당됩니다. 따라서 indexOf목록을 스캔 할 때 순차 메모리 주소를 통과합니다. 이는 CPU 캐시 프리 페치 휴리스틱에 적합합니다.

그러나 목록을 정렬 한 후에도 평균적으로 동일한 수의 메모리 조회를 수행해야하지만 이번에는 메모리 액세스가 임의의 순서로 이루어집니다.

최신 정보

다음은 할당 된 객체의 순서가 중요하다는 것을 증명하는 벤치 마크 입니다.

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
이것이 사실이라면, 대신에 정렬의 셔플 같은 결과를 생성한다
데이비드 Soroko

1
@DavidSoroko 그렇습니다.
assylias

1
@DavidSoroko 벤치 마크 코드 하단에 정렬되지 않고, 섞이고, 정렬되고, 연속적으로 정렬 된 전체 벤치 마크 결과 .
assylias

1
@assylias 흥미로운 확장은 순차 번호를 만드는 것입니다 (그리고 여기에 결과 코드를 게시하면 내 대답이 쓸모 없게 될 것입니다).
Marco13

1
에 그냥 강조하기 위해 list.indexOf(list.get(index))(가) list.get(index)부터 프리 페치에서 어떤 식 으로든 도움이되지 않는 index임의이다. 의 가격은 list.get(index)목록이 정렬되었는지 여부에 관계없이 동일합니다. 에 대해서만에서 차기 프리 페치list.indexOf()
데이비드 Soroko

25

메모리 캐시 미스의 영향을보고 있다고 생각합니다.

정렬되지 않은 목록을 만들 때

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

모든 double은 인접한 메모리 영역에 할당 될 가능성이 높습니다. 이를 반복하면 캐시 누락이 거의 발생하지 않습니다.

반면에 정렬 된 목록에서 참조는 혼란스러운 방식으로 메모리를 가리 킵니다.

이제 연속 메모리가있는 정렬 된 목록을 만드는 경우 :

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

이 정렬 된 목록은 원래 목록 (내 타이밍)과 성능이 동일합니다.


8

간단한 예를 들어 그 확인하는 wero 의해 답변apangin 의해 답변 (+1!) : 다음은 두 옵션의 간단한 비교를 수행 :

  • 난수 생성 및 선택적으로 정렬
  • 순차 번호를 만들고 선택적으로 섞기

또한 JMH 벤치 마크로 구현되지는 않지만 원본 코드와 유사하며 효과를 관찰하기 위해 약간만 수정하면됩니다.

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);
    }

}

내 컴퓨터의 출력은

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

타이밍이 다른 타이밍과 정확히 반대임을 멋지게 보여줍니다. 임의의 숫자가 정렬되면 정렬 된 버전이 더 느립니다. 순차 번호를 섞으면 섞인 버전이 더 느립니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.