【发布时间】:2016-09-20 14:46:05
【问题描述】:
首先,请注意在 stackoverflow 上已经有很多类似的问题,例如this、this、this。但是,我想强调问题的另一个方面,我在这些讨论中没有看到答案:
对ArrayList(特别是ArrayList,而不是其他List 实现)进行三种迭代变体的基准测试:
变体 1:
int size = list.size();
for(int i = 0; i < size; i++)
doSomething(list.get(i));
变体 2:
for(Object element : list)
doSomething(element);
变体 3:
list.forEach(this::doSomething);
事实证明,在我的机器上,1 号和 3 号机器的性能相同,而 2 号机器的运行时间大约要长 55%。这证实了直觉,即在第 2 号中隐式创建的Iterator 在每次迭代期间比传统的数组循环(为此进行了数十年的编译器优化研究)所做的工作更多。 3 号变体当然在内部执行与 1 号相同的循环。
现在我的实际问题是:假设 3 号变体由于某种原因不适用(循环中的副作用或没有 Java 8),那么在 Java 中循环 List 的最干净的方法是什么?正如this 讨论中所述,对于其他Lists,变体1 的效率可能比ArrayList 低得多。牢记这一点,我们可以这样做:
if(list instanceof ArrayList)
doVariant1(list);
else
doVariant2(list);
不幸的是,这更像是引入样板代码。那么,您会推荐什么作为迭代 List 的干净且高性能的解决方案?
编辑:这里是那些想要重现结果的完整代码。我正在使用 JRE 1.8.0,JavaHotspot Sever VM build 25.0-b70 on MacOS 10 with 2 GHz Intel Core i7。
private static final int ARRAY_SIZE = 10_000_000;
private static final int WARMUP_COUNT = 100;
private static final int TEST_COUNT = 100 + (int)(Math.random() + 20);
public void benchmark()
{
for(int i = 0; i < WARMUP_COUNT; i++)
test();
long totalTime = 0;
for(int i = 0; i < TEST_COUNT; i++)
totalTime += test();
System.out.println(totalTime / TEST_COUNT);
}
private long test()
{
ArrayList<Object> list = new ArrayList<>(ARRAY_SIZE);
for(int i = 0; i < ARRAY_SIZE; i++)
list.add(null);
long t = System.nanoTime();
doLoop(list);
return System.nanoTime() - t;
}
private void doLoop(ArrayList<Object> list)
{ // replace with the variant to test.
for(Object element : list)
doSomething(element);
}
private void doSomething(Object element)
{
Objects.nonNull(element);
}
【问题讨论】:
-
我不会复制你的结果。对于索引 for 循环,我实际上每次运行只得到 125 ns,而其他情况下为 3-6 毫秒。显而易见的原因是完整的测试循环作为死代码被消除了。你的“黑洞”方法
doSomething有缺陷。 -
@MarkoTopolnik 作为替代品,您有什么建议?
-
好吧,增加一个简单的静态 int 计数器会有所不同。在那种情况下,我的结果是“增强的”速度慢了两倍。当然,打印计数器的值也很重要。
-
这很奇怪...我尝试了打印 int 计数器的解决方案,但仍然得到类似的结果。
-
这是因为你无论如何都没有得到DCE。对于有缺陷的基准代码来说,不可重现的结果是典型的。
标签: java arrays performance arraylist