用于迭代ArrayList的干净且高效的方式

时间:2016-09-20 14:46:06

标签: java arrays performance arraylist

首先,请注意stackoverflow上已经有很多类似的问题,例如: thisthisthis。但是,我想强调问题的另一个方面,我在这些讨论中没有看到答案:

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讨论中所提到的,对于其他List而言,1号变体的效率可能远低于ArrayList。记住这一点,我们可以这样做:

if(list instanceof ArrayList)
    doVariant1(list);
else
    doVariant2(list);

不幸的是,这有点像引入样板代码。那么,你会推荐什么作为迭代List的干净,高效的解决方案?

编辑:以下是希望重现结果的完整代码。我在MacOS 10上使用JRE 1.8.0,JavaHotspot Sever VM build 25.0-b70和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);
}

2 个答案:

答案 0 :(得分:3)

  1. 你的&#34;黑洞&#34;方法doSomething,它应该使用迭代的值,什么也不做,并成为死代码消除优化的简单目标。当我尝试重现你的结果时,在indexedFor的情况下每次运行只有125纳秒,而在其他两种情况下则为3-6毫秒。

  2. 在修复之后(通过递增static int counter并在最后打印它),&#34;增强了&#34;变量比其他两个慢两倍,一般范围是每次运行5-9毫秒。为了保持透视,每个元素的亮度为0.5-0.9纳秒。

  3. 您的测试的相关性几乎没有,因为它试图测量迭代的纯开销而不实际对元素做任何事情。实际上没有元素,只有空值。

  4. 如果我们使用如下所示的修改代码,它会对每个元素执行最少的工作(取消引用其int字段并将其添加到计数器中),速度差异几乎消失:9.5 ms &#34;增强了&#34;而其他两种情况则为8毫秒。几乎不可能编写一个有用的程序,其中差异实际上很重要。

  5. 总之:没有关于&#34;最佳成语的建议&#34;会出来的。选择满足其他一些标准的习语,如可读性和简单性。索引for循环是最复杂的,因为额外的i变量和检查需要确保您处理实际上受益于它的实现。

    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);
    
    private static int counter;
    
    public static 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);
        System.out.println("Nonnull count: " + counter);
    }
    
    private static long test() {
        final ArrayList<Integer> list = new ArrayList<>(ARRAY_SIZE);
        for (int i = 0; i < ARRAY_SIZE; i++)
            list.add(i % 256 - 128);
        final long start = System.nanoTime();
        forEach(list);
        return System.nanoTime() - start;
    }
    
    private static void indexedFor(ArrayList<Integer> list) {
        for (int i = 0; i < list.size(); i++) {
            doSomething(list.get(i));
        }
    }
    
    private static void enhancedFor(ArrayList<Integer> list) {
        for (Integer element : list)
            doSomething(element);
    }
    
    private static void forEach(ArrayList<Integer> list) {
        list.forEach(Testing::doSomething);
    }
    
    private static void doSomething(Integer element) {
        counter += element.intValue();
    }
    
    public static void main(String[] args) {
        benchmark();
    }
    

答案 1 :(得分:0)

你真的需要这样的微观表现吗?因为你要求的是最干净的&#34;方式,2)和(3)是最好的。此外,(2)和(3)总体上具有更好的性能,因为具体类将提供其自己最合适的迭代器,并且通常是最好的迭代器。

(2)对基于数组的List具有良好的性能,但对其他(例如LinkedList)有害。但是,即使使用ArrayList,大多数真实世界的应用程序也不会在(1)和(2)/(3)之间看到很大的性能差异,因为大部分CPU时间将用在doSomething()上而不是循环列表。 / p>