ChatGPT解决这个技术问题 Extra ChatGPT

为什么处理排序数组比未排序数组*慢*? (Java 的 ArrayList.indexOf)

标题参考了 Why is it faster to process a sorted array than an unsorted array?

这也是分支预测效应吗?注意:这里排序数组的处理速度较慢!!

考虑以下代码:

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 提供 the results

/**
 * 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
 */
如果您想要有意义的结果,请使用 JMH 等适当的基准测试框架重新进行测量。
此外,即使没有 JMH,您的测试方法在概念上也存在缺陷。您正在测试各种事物,包括 RNG、System.currentTimeMillisassertEquals。没有预热迭代,通常没有迭代,您依赖于恒定的时间量并检查在那段时间内完成了多少。对不起,但这个测试实际上是没有用的。
使用 jmh 获得类似的结果...

a
apangin

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

线索是您比较双精度数(对象),而不是双精度数(基元)。当您在一个线程中分配对象时,它们通常在内存中按顺序分配。因此,当 indexOf 扫描一个列表时,它会遍历连续的内存地址。这对于 CPU 缓存预取启发式方法很有用。

但是在对列表进行排序之后,您仍然需要平均执行相同数量的内存查找,但这一次内存访问将是随机顺序的。

更新

Here is the benchmark 证明分配对象的顺序很重要。

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

如果这是真的,改组而不是排序应该产生相同的结果
@DavidSoroko 确实如此。
@DavidSoroko 完整的基准测试结果,在 the benchmark code 的底部未排序、打乱、排序和排序连续。
@assylias 一个有趣的扩展也可能是创建序列号(并在此处发布生成的代码会使我的答案过时)。
只是强调一下,在 list.indexOf(list.get(index)) 中,list.get(index) 不会以任何方式从预取中受益,因为 index 是随机的。无论列表是否排序,list.get(index) 的价格都是相同的。仅对 list.indexOf() 进行预取
w
wero

我认为我们正在看到内存缓存未命中的影响:

创建未排序列表时

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

所有双精度最有可能分配在连续的内存区域中。遍历它会产生很少的缓存未命中。

另一方面,在排序列表中,引用以混乱的方式指向内存。

现在,如果您创建一个具有连续内存的排序列表:

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

此排序列表与原始列表具有相同的性能(我的时间)。


C
Community

作为确认 answer by weroanswer 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

很好地表明时间正好与另一个相反:如果对随机数进行排序,则排序后的版本会更慢。如果序列号被打乱,那么打乱的版本会更慢。