web-dev-qa-db-ja.com

ソートされた配列を処理するのは、ソートされていない配列よりも*遅い*のはなぜですか? (JavaのArrayList.indexOf)

タイトルは ソートされていない配列よりもソートされた配列を処理するほうが速いのはなぜですか?

これも分岐予測効果ですか?注意:ここでは、ソートされた配列の処理はslower!!

次のコードを検討してください。

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テストで実行されます。

[〜#〜] update [〜#〜]

連続したメモリアクセスに関連しているようです(シーケンシャル順とランダム順のダブルオブジェクト)。配列の長さが約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
 */
80
user1050755

キャッシング/プリフェッチ効果のように見えます。

手がかりは、ダブル(プリミティブ)ではなく、ダブル(オブジェクト)を比較することです。 1つのスレッドでオブジェクトを割り当てると、通常はメモリ内で順番に割り当てられます。したがって、indexOfがリストをスキャンするとき、シーケンシャルメモリアドレスを調べます。これは、CPUキャッシュのプリフェッチヒューリスティックに適しています。

ただし、リストを並べ替えた後も、平均して同じ数のメモリルックアップを行う必要がありますが、今回はメモリアクセスがランダムな順序で行われます。

[〜#〜] update [〜#〜]

これがベンチマークです 割り当てられたオブジェクトの順序が重要であることを証明します。

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
87
apangin

メモリキャッシュミスの影響を見ていると思います。

ソートされていないリストを作成するとき

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

すべてのdoubleは、ほとんどの場合、連続したメモリ領域に割り当てられます。これを繰り返すと、キャッシュミスはほとんど発生しません。

一方、ソートされたリストでは、参照は混chaとした方法でメモリを指します。

連続したメモリでソートされたリストを作成する場合:

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

このソートされたリストのパフォーマンスは、元のリストと同じです(私のタイミング)。

25
wero

answer by wero および answer by 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

タイミングが別のものとまったく逆であることをうまく示しています:乱数がソートされている場合、ソートされたバージョンは遅くなります。連続番号がシャッフルされると、シャッフルされたバージョンは遅くなります。

8
Marco13