为什么处理排序数组比未排序数组慢?(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下运行,在Eclipse Mars中进行了JUnit测试。

更新

似乎与连续的内存访问有关(按顺序与随机顺序访问Double对象)。对于大约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 '16

7
同样,即使没有JMH,您的测试方法在概念上也存在缺陷。您正在测试各种各样的东西,包括RNGSystem.currentTimeMillisassertEquals。没有热身迭代,通常没有迭代,您依靠固定的时间量并检查在那段时间内完成了多少操作。抱歉,此测试实际上是无用的。
Clashsoft '16

4
使用jmh获得类似的结果...
assylias 2016年

Answers:


88

看起来像是缓存/预取效果。

提示是您比较Doubles(对象),而不是Doubles(基元)。在一个线程中分配对象时,通常会在内存中按顺序分配对象。因此,当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

1
@DavidSoroko确实如此。
assylias,2016年

1
@DavidSoroko完整的基准测试结果,在基准代码的底部带有未排序,乱序,排序和连续的内容。
assylias,2016年

1
@assylias一个有趣的扩展可能是还创建了序列号(在此处发布结果代码会使我的答案过时)。
Marco13 '16

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

很好地显示了计时恰好与另一个计时相反:如果对随机数进行排序,则排序后的版本会更慢。如果改组了序号,那么改组的版本会更慢。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.