标题参考了 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
*/
System.currentTimeMillis
和 assertEquals
。没有预热迭代,通常没有迭代,您依赖于恒定的时间量并检查在那段时间内完成了多少。对不起,但这个测试实际上是没有用的。
它看起来像缓存/预取效果。
线索是您比较双精度数(对象),而不是双精度数(基元)。当您在一个线程中分配对象时,它们通常在内存中按顺序分配。因此,当 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
我认为我们正在看到内存缓存未命中的影响:
创建未排序列表时
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()));
}
此排序列表与原始列表具有相同的性能(我的时间)。
作为确认 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
很好地表明时间正好与另一个相反:如果对随机数进行排序,则排序后的版本会更慢。如果序列号被打乱,那么打乱的版本会更慢。
list.indexOf(list.get(index))
中,list.get(index)
不会以任何方式从预取中受益,因为index
是随机的。无论列表是否排序,list.get(index)
的价格都是相同的。仅对list.indexOf()
进行预取