ソートされた配列の処理が、ソートされていない配列よりも*遅い*のはなぜですか?(Javaの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、EclipseMarsの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 2016年

7
また、JMHがなくても、テスト方法には概念上の欠陥があります。あなたはRNGを含め、あらゆるものをテストし、されているSystem.currentTimeMillisassertEquals。ウォームアップの反復はありません。一般に反復はありません。一定の時間に依存し、その時間に実行された量を確認します。申し訳ありませんが、このテストは事実上役に立たないです。
Clashsoft 2016年

4
... JMHと同様の結果を取得する
assylias

回答:


88

キャッシュ/プリフェッチ効果のようです。

手がかりは、double(プリミティブ)ではなく、Double(オブジェクト)を比較することです。オブジェクトを1つのスレッドに割り当てる場合、通常、オブジェクトはメモリ内で順番に割り当てられます。したがって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
これが当てはまる場合、並べ替えの代わりにシャッフルすると同じ結果が得られるはずです
David Soroko 2016年

1
@DavidSorokoそうです。
assylias 2016年

1
@DavidSorokoベンチマークコードの下部に、並べ替えられていない、シャッフルされた、並べ替えられた、並べ替えられた連続した完全なベンチマーク結果。
assylias 2016年

1
@assylias興味深い拡張機能は、連番も作成することです(結果のコードをここに投稿すると、私の答えは時代遅れになります)。
Marco13 2016年

1
強調するために、はランダムであるため、プリフェッチのメリットはまったくlist.indexOf(list.get(index))ありlist.get(index)ませんindex。の価格はlist.get(index)、リストがソートされているかどうかに関係なく同じです。プリフェッチは次の場合にのみ開始されますlist.indexOf()
David 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.