for循环优化

46
List<String> flowers = new ArrayList<String>();

我的for循环目前看起来像这样...

for (int i = 0; i < flowers.size(); i++) {
...
}

我应该将这个代码更改为下面给出的代码吗?

int size = flowers.size();
for (int i = 0; i < size; i++) {
...
}

如果我有一个大的花卉数组,哪种方式更高效?我猜应该是后者。


14
我认为潜在的机制可能会为您进行优化。 - sje397
希望Java编译器能够为您进行优化。 - Yaur
如果Java没有进行优化,那么避免每次调用.size()方法后者确实更好。但是Java非常聪明。 - Owen
9
@sje397,@Yaur:是否能对此进行优化取决于JVM和循环内的代码。如果循环内的代码更改了flowers的大小(这可能非常棘手难以检查!),那么就无法进行优化。 - Joachim Sauer
...而且JVM必须保持悲观:它需要证明循环内部的任何代码(包括其中调用的任何函数)都不能更改“flowers”。这在现代语言(如Java)中尤其困难,因为代码可以在运行时更改,例如通过加载额外的代码。 - MSalters
3
不行优化,因为 size() 可能会产生副作用。 - Adam Dyga
15个回答

60

最好使用for-each循环 [更易读]

for (Flower flower :flowers){
    //...
}

我使用javap命令对以下代码进行了指令转储:

public void forLoop1() {
    List<String> lst = new ArrayList<String>();
    for (int i = 0; i < lst.size(); i++) {
        System.out.println("hi");
    }
}

public void forLoop2() {
    List<String> lst = new ArrayList<String>();
    int size = lst.size();
    for (int i = 0; i < size; i++) {
        System.out.println("hi");
    }
}

public void forLoop1();
  Code:
   0:   new     #2; //class java/util/ArrayList
   3:   dup
   4:   invokespecial   #3; //Method java/util/ArrayList."<init>":()V
   7:   astore_1
   8:   iconst_0
   9:   istore_2
   10:  iload_2
   11:  aload_1
   12:  invokeinterface #4,  1; //InterfaceMethod java/util/List.size:()I
   17:  if_icmpge       34
   20:  getstatic       #5; //Field java/lang/System.out:Ljava/io/PrintStream;
   23:  ldc     #6; //String hi
   25:  invokevirtual   #7; //Method java/io/PrintStream.println:(Ljava/lang/Str
ing;)V
   28:  iinc    2, 1
   31:  goto    10
   34:  return

public void forLoop2();
  Code:
   0:   new     #2; //class java/util/ArrayList
   3:   dup
   4:   invokespecial   #3; //Method java/util/ArrayList."<init>":()V
   7:   astore_1
   8:   aload_1
   9:   invokeinterface #4,  1; //InterfaceMethod java/util/List.size:()I
   14:  istore_2
   15:  iconst_0
   16:  istore_3
   17:  iload_3
   18:  iload_2
   19:  if_icmpge       36
   22:  getstatic       #5; //Field java/lang/System.out:Ljava/io/PrintStream;
   25:  ldc     #6; //String hi
   27:  invokevirtual   #7; //Method java/io/PrintStream.println:(Ljava/lang/Str
ing;)V
   30:  iinc    3, 1
   33:  goto    17
   36:  return

它对我没有进行优化。

java版本号为"1.6.0_22”,Java(TM) SE Runtime Environment(构建1.6.0_22-b04)Java HotSpot(TM) Client VM(构建17.1-b03,混合模式,共享)

因此,如果您需要从提到的两个中选择一个,请选择第二个,但我个人会选择for-each


for-each性能

摘自 Joshua Bloch 的Effective Java第46条:

在1.5版中引入的for-each循环通过隐藏迭代器或索引变量完全消除了混乱和错误的机会。由此产生的习语同样适用于集合和数组:

// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    doSomething(e);
}
当您看到冒号(:)时,将其解读为“在”的意思。因此,上面的循环可以理解为“对于每个元素e在elements中”。请注意,即使对于数组,使用for-each循环也不会带来性能损失。实际上,在某些情况下,它可能比普通的for循环提供轻微的性能优势,因为它仅计算一次数组索引的限制。虽然程序员可以手动完成这一点(Item 45),但并非总是这样做。

另请参阅


12
那不是问题,解释哪个结构是最好的,这只是一条评论... - dacwe
2
这不是一个好的答案。Java的for each循环比OP的问题慢两倍,比优化后的循环慢4倍。 - David Titarenco
7
这是一个很好的回答,因为当你整体表现不佳时,你最后应该做的就是进行微调优化。性能问题可以通过多线程和查看复杂度(O-表示法,包括选择适当的集合类型)来解决。对于易读性的评价非常高。有时候,不去想微调反而会有所帮助。(这是一般性陈述,Joe提出这个问题可能有很好的原因。) - Andreas Dolk
23
"它对我来说并不优化。" -- FYI,javac 很少进行优化。这些优化是由 JIT 编译器完成的。 - aioobe
2
多么扎实的答案啊!!!我正在忙于阅读《Effective Java》。这也是一本很棒的读物。唯一需要检查的额外事项是flowers是否为空。但它不适用于上面的例子。 - Koekiebox
显示剩余2条评论

19

很抱歉要说,但 @Jigar 的答案是错误的。这才是正确的答案。(简而言之,不要使用 for : each)。

import java.util.ArrayList;
import java.util.List;

public class LoopTest {

    public static void main(String s[]) {

        long start, end;

        List<Integer> a =  new ArrayList<Integer>();

        for (int i = 0; i < 2500000; i++) {
            a.add(i);
        }

        ///// TESTING FOR : EACH LOOP

        start = System.currentTimeMillis();

        for (Integer j : a) {
            int x = j + 3;
        }

        end = System.currentTimeMillis();

        System.out.println(end - start
                + " milli seconds for [ Integer j : a ] ");

        ////// TESTING DEFAULT LOOP

        start = System.currentTimeMillis();
        for (int i = 0; i < a.size(); i++) {
            int x = a.get(i) + 3;
        }

        end = System.currentTimeMillis();

        System.out.println(end - start
                + " milli seconds for [ int i = 0; i < a.length; i++ ] ");


        ////// TESTING SLIGHTLY OPTIMIZED LOOP

        start = System.currentTimeMillis();
        int size = a.size();
        for (int i = 0; i < size; i++) {
            int x = a.get(i) + 3;
        }

        end = System.currentTimeMillis();

        System.out.println(end - start
                + " milli seconds for [ int i = 0; i < size; i++ ] ");        

        //// TESTING MORE OPTIMIZED LOOP

        start = System.currentTimeMillis();
        for (int i = size; --i >= 0;) {
            int x = a.get(i) + 3;
        }

        end = System.currentTimeMillis();

        System.out.println(end - start
                + " milli seconds for [ int i = size; --i >= 0; ] ");       

    }

}

结果:

96 milli seconds for [ Integer j : a ] 
57 milli seconds for [ int i = 0; i < a.length; i++ ] 
31 milli seconds for [ int i = 0; i < size; i++ ] 
31 milli seconds for [ int i = size; --i >= 0; ] 

你可以自行判断,但是将太多功劳归于JVM优化器是不合适的。你仍然需要在自己的代码中做出明智的选择,并且几乎从来不建议使用for:each语法。正如你所看到的,将size放入它自己的变量中是个好主意。

尽管这些优化可能依赖于JVM(其中一些可能会随着JIT而启动),但重要的是要知道Java做了什么,以及Java没做什么。


3
你说得对,for : each 始终速度较慢,而频繁调用 .size() 比分配到一个变量中要慢一些。 - Ravi Parekh
4
说“几乎从来不使用 for-each 符号”是相当愚蠢的。在绝大多数情况下,你会更喜欢可读性而非性能。只有在循环成为性能瓶颈的少数情况下,才值得探究是否存在更高效的选项。 - Alderath
13
我认为你的 for each 代码样本存在自动装箱和拆箱的问题。 - Raze
对于每个循环也会创建一个迭代器 - 一个在堆中的对象,因此大量的 for each 可能会增加内存压力。 - Denis Gladkiy
@dfa - 你意识到这个答案已经有10年的历史了吗?而且它比JMH早几年吗? - David Titarenco

11

JVM 无法对其进行优化,因为 size() 是一个方法,JVM 在这种情况下无法(也不会尝试)确定 size() 始终会返回相同的值。只要 size() 的值不变,第二个方案略微更有效率,但效益非常小,你甚至不必考虑使用它。


9
如果性能很重要,使用简单的计数器循环,但对于98%的情况来说,代码的清晰度和简洁性更加重要(相当于1000倍或更多),应该使用for-each循环。
@David指出使用计数器更快,但即使是10000个条目,差异也是亚微秒级别。
如果您有一组超过10000个条目,则极有可能不应该 brute force 迭代每种可能性。更可能的是,具有类似Map的查找的集合是您所考虑的更好的解决方案。如果您的条目远少于10000,则性能不太重要。

6

如果在迭代期间更改了数组列表,则行为会不同。但我猜你不这样做。根据我的测试,后者通常更快(尤其是在像安卓这样的系统上)。我会这样写:

for (int i = 0, size = flowers.size(); i < size; i++) {
    ...
}

4

根据Java语言规范(14.14.1):

基本的for语句执行一些初始化代码,然后执行一个表达式,一个语句和一些更新代码重复,直到表达式的值为false。

在您的第一个示例中,表达式是i < flowers.size(),并且在每次迭代中都会被评估一次。在您的特殊情况下,这不应该产生显着差异,因为ArrayList上的flowers.getSize()是一个非常短的方法。但是,通常情况下,如果表达式的结果对于每次迭代都相同且昂贵,则进行预计算。

结果:这必须在Java虚拟机的每个实现中产生相同的输出,并证明表达式在每次迭代中只评估一次:

int counter2 = 10;
for (int counter1 = 0; counter1 < counter2; counter1++) {
  System.out.println(counter1 + ", " + counter2);
  counter2--;
}

输出:

0, 10
1, 9
2, 8
3, 7
4, 6

3
最佳选择是:
[ int i = 0; i < size; i++ ]

您的结果会因使用哪个JVM和其他设置(例如-client vs -server)而异,因为某些测量值非常微小,您需要使用纳秒来进行测量,并且需要进行多次测试,否则您最终会发现垃圾回收正在干扰结果。此外,这些类型的测试往往会使JVM将循环优化为无效操作。我尝试通过将修改变量放置在代码末尾以显示在屏幕上来消除这种风险。
1.6
-server
7.968242071 milli seconds for [ Integer j : a ] 
7.206275775999999 milli seconds for [ int i = 0; i < a.length; i++ ]  
1.5864E-5 milli seconds for [ int i = 0; i < size; i++ ] 
14.774186076999998 milli seconds for [ int i = size; --i >= 0; ] 

-client
83.36101683999999 milli seconds for [ Integer j : a ] 
44.288568631 milli seconds for [ int i = 0; i < a.length; i++ ]  
2.3191E-5 milli seconds for [ int i = 0; i < size; i++ ] 
24.826621246 milli seconds for [ int i = size; --i >= 0; ] 

1.7

-server
7.029150422 milli seconds for [ Integer j : a ] 
6.6269827779999995 milli seconds for [ int i = 0; i < a.length; i++ ]  
1.3852E-5 milli seconds for [ int i = 0; i < size; i++ ] 
13.842110377 milli seconds for [ int i = size; --i >= 0; ] 
13.868426141 milli seconds for [ int i = a.size()-1; i >= 0; i-- ] 
1.6618000000000003E-5 milli seconds for [ int i = 0; i < a.size(); i++ ] 

-client
7.382479727 milli seconds for [ Integer j : a ] 
6.748068759 milli seconds for [ int i = 0; i < a.length; i++ ]  
1.4162999999999998E-5 milli seconds for [ int i = 0; i < size; i++ ] 
13.951547335999999 milli seconds for [ int i = size; --i >= 0; ] 
13.929234053999998 milli seconds for [ int i = a.size()-1; i >= 0; i-- ] 
1.6873E-5 milli seconds for [ int i = 0; i < a.size(); i++ ] 

测试代码:

public static void main(String s[]) {
long start=0, end = 0, delta = 0;
//int[] a = new int[2500000];
List<Integer> a = new ArrayList<Integer>();
int x = 0;

for (int i = 0; i < 2500000; i++) {
    a.add(i);
}

start=0; end = 0; delta = 0;
for (int ctr = 0; ctr < 1000; ctr++) {
    start = System.nanoTime();
    for (Integer j : a) {
         x = j + 3;
    }
    end = System.nanoTime();
    delta += end - start;
}
System.out.println(Math.pow(10, -6) * delta / 1000 + " milli seconds for [ Integer j : a ] ");


start=0; end = 0; delta = 0;
for (int ctr = 0; ctr < 1000; ctr++) {
    start = System.nanoTime();
    for (int i = 0; i < a.size(); i++) {
         x = a.get(i) + 3;
    }
    end = System.nanoTime();
    delta += end - start;
}
System.out.println(Math.pow(10, -6) * delta / 1000 + " milli seconds for [ int i = 0; i < a.length; i++ ]  ");

int size = a.size();

start=0; end = 0; delta = 0;
for (int ctr = 0; ctr < 1000; ctr++) {
    start = System.currentTimeMillis();

    for (int i = 0; i < size; i++) {
         x = a.get(i) + 3;
    }
    end = System.currentTimeMillis();
    delta += end - start;
}
System.out.println(Math.pow(10, -6) * delta / 1000 + " milli seconds for [ int i = 0; i < size; i++ ] ");

start=0; end = 0; delta = 0;
for (int ctr = 0; ctr < 1000; ctr++) {
    start = System.nanoTime();
    for (int i = size; --i >= 0;) {
         x = a.get(i) + 3;
    }
    end = System.nanoTime();
    delta += end - start;
}
System.out.println(Math.pow(10, -6) * delta / 1000 + " milli seconds for [ int i = size; --i >= 0; ] ");


start=0; end = 0; delta = 0;
for (int ctr = 0; ctr < 1000; ctr++) {
    start = System.nanoTime();
    for (int i = a.size()-1; i >= 0; i--) {
         x = a.get(i) + 3;
    }
    end = System.nanoTime();
    delta += end - start;
}
System.out.println(Math.pow(10, -6) * delta / 1000 + " milli seconds for [ int i = a.size()-1; i >= 0; i-- ] ");

start=0; end = 0; delta = 0;
for (int ctr = 0; ctr < 1000; ctr++) {
    start = System.currentTimeMillis();

    for (int i = 0; i < a.size(); i++) {
         x = a.get(i) + 3;
    }
    end = System.currentTimeMillis();
    delta += end - start;
}
System.out.println(Math.pow(10, -6) * delta / 1000 + " milli seconds for [ int i = 0; i < a.size(); i++ ] ");        

System.out.println(x);
}

不错的数据收集。但是有一些问题:1. 不要切换使用的计时器!(它们返回不同的单位,因此报告的值是不正确的!)2. 更新以获得一致的结果和格式,使所有数字都使用相同的基数3. 在同一进程中多次运行整个测试集以检查异常情况。另外,“最快”不一定是“最好”的;-) - user166390

3

另外,如果你想知道使用方法调用作为源集合是否会有任何性能影响。也就是说 - 方法会被调用多次吗?答案是否定的。以下是一个例子:

import java.util.*;
public class TestForeach {
    public static void main (String[] args) {

        for (String s : getStrings()) {
            System.out.println("The string was: "+s);
        }
    } 

    private static List<String> getStrings() {
        System.out.println("IN GET STRINGS");
        return Arrays.asList("A","B","C");
    }
}

这将导致:
IN GET STRINGS
The string was: A
The string was: B
The string was: C

因此,该方法只会被调用一次。

2
这只是一个举例说明这个问题的情境。
我测试了"normal" for循环for (int i = 0; i < list.size(); i++)和微优化的for循环for (int i = -1, size = list.size(); ++i < size;)。我在eclipse和命令行中都运行了测试,并发现了巨大的差异。
在eclipse中运行的结果如下:
Time for Original: 32552 ms   Time for MicroOptimized 32707 ms
Fastest Loop: Original
Slowest loop takes 0.47616121897272057% more time

从命令行运行的结果:

Time for Original: 274489 ms   Time for MicroOptimized 30516 ms
Fastest Loop: MicroOptimized
Slowest loop takes 799.4920697339101% more time

在eclipse中,这两个for循环的时间相同,但是从命令行运行时,原始版本比微优化版本慢了800%。这种差异的程度让我惊讶。我猜测eclipse使用了一个不同的JVM,应用了一些智能优化技巧。
然而,这并不意味着你应该开始使用微优化版本。在几乎所有情况下,你要迭代的列表可能会非常小,性能差异可以忽略不计。从使用标准版获得的可读性和更快的理解速度,将更有益于你,而不是一种无法感知的性能提升。
为了完整起见,以下是我运行的代码:
public static void main(String[] args) {
        List<Byte> list = initializeList();
        byte value = 0;
        final int NUM_LOOPS = 100;

        long startOriginal, startOptimized, endOriginal, endOptimized;

        startOptimized = System.currentTimeMillis();
        for (int j = 0; j < NUM_LOOPS; j++) {
            for (int i = -1, size = list.size(); ++i < size;) {
                value = list.get(i);
            }
        }
        endOptimized = System.currentTimeMillis();

        startOriginal = System.currentTimeMillis();
        for (int j = 0; j < NUM_LOOPS; j++) {
            for (int i = 0; i < list.size(); i++) {
                value = list.get(i);
            }
        }
        endOriginal = System.currentTimeMillis();

        System.out.println(value);
        printResults(startOriginal, endOriginal, startOptimized, endOptimized);
    }

    private static void printResults(long startOriginal, long endOriginal,
            long startOptimized, long endOptimized) {

        long timeOriginal = endOriginal - startOriginal;
        long timeOptimized = endOptimized - startOptimized;

        long diff = Math.abs(timeOriginal - timeOptimized);
        long min = Math.min(timeOriginal, timeOptimized);

        System.out.println("Time for Original: " + timeOriginal + " ms"
                + "   Time for MicroOptimized " + timeOptimized + " ms");

        System.out.println("Fastest Loop: "
                + ((timeOriginal < timeOptimized) ? "Original"
                        : "MicroOptimized"));

        System.out.println("Slowest loop takes " + ((double) 100 * diff / min)
                + "% more time");       
    }

    public static List<Byte> initializeList(){
        List<Byte> list = new ArrayList<Byte>();
        final Byte ONE = new Byte((byte) 1);

        for (int i = 0; i < Integer.MAX_VALUE / 10; i++) {
            list.add(ONE);
        }

        return list;
    }
}

1

两种方法都可以。根据JVM的不同,第二种方法可能会快几个时钟周期,但这只是微不足道的差异。要注意这些类型的次优化。除非您正在构建实时系统,在那里每个CPU时钟都很重要,否则它们只会增加复杂性和错误来源。

我建议使用迭代器结构(如已经建议的那样)。

for (Flower flower: flowers) { ...

代码清晰易懂,灵活可扩展,运行结果可预知。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接