【问题标题】:Clean and performant way for iterating over an ArrayList用于迭代 ArrayList 的干净且高效的方式
【发布时间】:2016-09-20 14:46:05
【问题描述】:

首先,请注意在 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 讨论中所述,对于其他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


【解决方案1】:
  1. 您的“黑洞”方法doSomething,应该利用迭代的值,什么都不做,成为死代码消除优化的一个简单目标。当我尝试重现您的结果时,indexedFor 的每次运行时间仅为 125 纳秒,而其他两种情况下为 3-6 毫秒。

  2. 在修复之后(通过增加 static int counter 并在最后打印),“增强版”变体比其​​他两个慢两倍,一般范围是每次运行 5-9 毫秒。为了让事情更清晰,这是每个元素 0.5-0.9 纳秒。

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

  4. 如果我们使用如下所示的修改后的代码,每个元素的工作量最少(取消引用其int 字段并将其添加到计数器),速度差异几乎消失:“增强型”为 9.5 毫秒为“与其他两种情况下为 8 毫秒。几乎不可能编写一个有用的程序,而这种差异实际上很重要。

总结:不会由此产生“最佳成语”的建议。选择满足其他一些标准的成语,例如可读性和简单性。索引 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();
}

【讨论】:

    【解决方案2】:

    你真的需要这样的微性能吗?因为您要求“最干净”的方式,所以 2) 和 (3) 是最好的。此外,(2) 和 (3) 总体上具有更好的性能,因为具体类将提供自己最合适的迭代器,而这通常是最好的。

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

    【讨论】:

    • 好点,这是一个在日常编程中可能不太相关的问题。而是出于学术兴趣。
    猜你喜欢
    • 1970-01-01
    • 2015-12-11
    • 1970-01-01
    • 1970-01-01
    • 2010-10-16
    • 2011-06-25
    • 2013-12-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多