Java for循环性能问题

时间:2010-03-04 23:10:31

标签: java performance compiler-construction for-loop microbenchmark

考虑这个例子:

public static void main(final String[] args) {
    final List<String> myList = Arrays.asList("A", "B", "C", "D");
    final long start = System.currentTimeMillis();
    for (int i = 1000000; i > myList.size(); i--) {
        System.out.println("Hello");
    }
    final long stop = System.currentTimeMillis();
    System.out.println("Finish: " + (stop - start));
}

vs

public static void main(final String[] args) {
    final List<String> myList = Arrays.asList("A", "B", "C", "D");
    final long start = System.currentTimeMillis();
    final int size = myList.size();
    for (int i = 1000000; i > size; i--) {
        System.out.println("Hello");
    }
    final long stop = System.currentTimeMillis();
    System.out.println("Finish: " + (stop - start));
}

这会产生任何差异吗?在我的机器上,第二个似乎表现得更快,但我不知道它是否真的准确。编译器会优化此代码吗?如果循环条件是一个不可变对象(例如String数组),我可以认为他会这样做。

13 个答案:

答案 0 :(得分:24)

如果你想测试这样的东西,你必须优化你的微基准测量来测量你关心的东西。

首先,使循环便宜无法跳过。计算总和通常可以解决问题。

其次,比较两个时间。

这里有一些代码同时执行:

import java.util.*;

public class Test {

public static long run1() {
  final List<String> myList = Arrays.asList("A", "B", "C", "D");
  final long start = System.nanoTime();
  int sum = 0;
  for (int i = 1000000000; i > myList.size(); i--) sum += i;
  final long stop = System.nanoTime();
  System.out.println("Finish: " + (stop - start)*1e-9 + " ns/op; sum = " + sum);
  return stop-start;
}

public static long run2() {
  final List<String> myList = Arrays.asList("A", "B", "C", "D");
  final long start = System.nanoTime();
  int sum = 0;
  int limit = myList.size();
  for (int i = 1000000000; i > limit; i--) sum += i;
  final long stop = System.nanoTime();
  System.out.println("Finish: " + (stop - start)*1e-9 + " ns/op; sum = " + sum);
  return stop-start;
}

public static void main(String[] args) {
  for (int i=0 ; i<5 ; i++) {
    long t1 = run1();
    long t2 = run2();
    System.out.println("  Speedup = " + (t1-t2)*1e-9 + " ns/op\n");
  }
}

}

如果我们运行它,在我的系统上我们得到:

Finish: 0.481741256 ns/op; sum = -243309322
Finish: 0.40228402 ns/op; sum = -243309322
  Speedup = 0.079457236 ns/op

Finish: 0.450627151 ns/op; sum = -243309322
Finish: 0.43534661700000005 ns/op; sum = -243309322
  Speedup = 0.015280534 ns/op

Finish: 0.47738474700000005 ns/op; sum = -243309322
Finish: 0.403698331 ns/op; sum = -243309322
  Speedup = 0.073686416 ns/op

Finish: 0.47729349600000004 ns/op; sum = -243309322
Finish: 0.405540508 ns/op; sum = -243309322
  Speedup = 0.071752988 ns/op

Finish: 0.478979617 ns/op; sum = -243309322
Finish: 0.36067492700000003 ns/op; sum = -243309322
  Speedup = 0.11830469 ns/op

这意味着方法调用的开销约为0.1 ns。如果你的循环执行的时间不超过1-2 ns,那么你应该关心这一点。否则,不要。

答案 1 :(得分:10)

就个人而言,我认为你不能从这样一个人为的例子中得出任何有意义的结论。

但是如果你真的想知道,为什么不使用javap反编译代码并看看有什么不同?为什么猜测编译器在你不知道的时候可以自己看到什么呢?

第一种情况的字节代码:

public class Stackoverflow extends java.lang.Object{
public Stackoverflow();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   iconst_4
   1:   anewarray       #2; //class java/lang/String
   4:   dup
   5:   iconst_0
   6:   ldc     #3; //String A
   8:   aastore
   9:   dup
   10:  iconst_1
   11:  ldc     #4; //String B
   13:  aastore
   14:  dup
   15:  iconst_2
   16:  ldc     #5; //String C
   18:  aastore
   19:  dup
   20:  iconst_3
   21:  ldc     #6; //String D
   23:  aastore
   24:  invokestatic    #7; //Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List
   27:  astore_1
   28:  invokestatic    #8; //Method java/lang/System.currentTimeMillis:()J
   31:  lstore_2
   32:  ldc     #9; //int 1000000
   34:  istore  4
   36:  iload   4
   38:  aload_1
   39:  invokeinterface #10,  1; //InterfaceMethod java/util/List.size:()I
   44:  if_icmple       61
   47:  getstatic       #11; //Field java/lang/System.out:Ljava/io/PrintStream;
   50:  ldc     #12; //String Hello
   52:  invokevirtual   #13; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   55:  iinc    4, -1
   58:  goto    36
   61:  invokestatic    #8; //Method java/lang/System.currentTimeMillis:()J
   64:  lstore  4
   66:  getstatic       #11; //Field java/lang/System.out:Ljava/io/PrintStream;
   69:  new     #14; //class java/lang/StringBuilder
   72:  dup
   73:  invokespecial   #15; //Method java/lang/StringBuilder."<init>":()V
   76:  ldc     #16; //String Finish:
   78:  invokevirtual   #17; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/la
   81:  lload   4
   83:  lload_2
   84:  lsub
   85:  invokevirtual   #18; //Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
   88:  invokevirtual   #19; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   91:  invokevirtual   #13; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   94:  return
}

第二种情况的字节代码:

public class Stackoverflow extends java.lang.Object{
public Stackoverflow();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   iconst_4
   1:   anewarray       #2; //class java/lang/String
   4:   dup
   5:   iconst_0
   6:   ldc     #3; //String A
   8:   aastore
   9:   dup
   10:  iconst_1
   11:  ldc     #4; //String B
   13:  aastore
   14:  dup
   15:  iconst_2
   16:  ldc     #5; //String C
   18:  aastore
   19:  dup
   20:  iconst_3
   21:  ldc     #6; //String D
   23:  aastore
   24:  invokestatic    #7; //Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
   27:  astore_1
   28:  invokestatic    #8; //Method java/lang/System.currentTimeMillis:()J
   31:  lstore_2
   32:  aload_1
   33:  invokeinterface #9,  1; //InterfaceMethod java/util/List.size:()I
   38:  istore  4
   40:  ldc     #10; //int 1000000
   42:  istore  5
   44:  iload   5
   46:  iload   4
   48:  if_icmple       65
   51:  getstatic       #11; //Field java/lang/System.out:Ljava/io/PrintStream;
   54:  ldc     #12; //String Hello
   56:  invokevirtual   #13; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   59:  iinc    5, -1
   62:  goto    44
   65:  invokestatic    #8; //Method java/lang/System.currentTimeMillis:()J
   68:  lstore  5
   70:  getstatic       #11; //Field java/lang/System.out:Ljava/io/PrintStream;
   73:  new     #14; //class java/lang/StringBuilder
   76:  dup
   77:  invokespecial   #15; //Method java/lang/StringBuilder."<init>":()V
   80:  ldc     #16; //String Finish:
   82:  invokevirtual   #17; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   85:  lload   5
   87:  lload_2
   88:  lsub
   89:  invokevirtual   #18; //Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
   92:  invokevirtual   #19; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   95:  invokevirtual   #13; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   98:  return
}

存在差异,但我不确定我是否可以就其对绩效的影响做出明确的陈述。

我会编写第二个代码,因为它表示(在它的表面上)一个方法调用,而不是每个循环迭代一个。我不知道编译器是否可以优化它,但我确信我可以很容易地做到这一点。所以我这样做,不管它对墙上时间的影响。

答案 2 :(得分:9)

我曾经参与过一个项目,我的第一个任务是追踪一些非常慢的代码(它是在一台全新的486机器上,执行大约需要20分钟):

for(size_t i = 0; i < strlen(data); i++)
{
    // do something with data[i]
}

解决方案是(将其降低到两分钟或更短的时间):

size_t length = strlen(data);

for(int i = 0; i < length; i++)
{
    // do something with data[i]
}

问题在于“数据”超过100万个字符,并且strlen必须始终统计每个字符。

对于Java,“size()”方法可能返回一个变量,因此,VM将内联它。在类似于Android上的VM上,它可能没有。所以答案是“它取决于”。

我个人的偏好是,如果每次都应该返回相同的结果,则不要多次调用方法。这样,如果该方法确实涉及计算,则只执行一次,然后它永远不会成为问题。

答案 3 :(得分:5)

请注意,javac编译器与优化无关。 “重要”编译器是JVM编译器,它位于JVM中。

在您的示例中,在最通用的情况下,myList.size()是一个简单的方法分派,它返回List实例中字段的内容。与System.out.println("Hello")暗示的相比,这是微不足道的工作(至少一个系统调用,因此数百个时钟周期,相比之下,方法调度不超过十几个)。我非常怀疑你的代码在速度方面会有明显的差异。

在更一般的基础上,JIT编译器应该将此size()的调用识别为对已知实例的调用,以便它可以使用直接函数调用(更快)执行方法调度,或者甚至内联size()方法调用,减少对简单实例字段访问的调用。

答案 4 :(得分:2)

它无法对其进行优化,因为mylist.size()可能会在循环执行期间发生变化。即使它是最终的,这只意味着引用是最终的(意味着你不能将myList重新分配给其他对象),但myList上的方法,例如remove()和add()仍然可用。 Final不会使对象不可变。

答案 5 :(得分:1)

第二个应该更快,因为每次执行循环时都不必调用.size()。比说多次说1 + 2 = 3要快得多。

答案 6 :(得分:1)

第二种实现更快是有意义的,因为您存储了变量的单个最终本地副本。编译器必须弄清楚循环内部的大小不能改变,以使性能大致相等。

一个问题是 - 这种微优化真的很重要吗?如果确实如此,那么请考虑测试中运行速度更快的内容,而不依赖于编译器优化。

答案 7 :(得分:1)

Java编译器会对它进行优化,但是没有看到有趣的条件。如果你这样写它就没有问题。

for (int i = myList.size(); i < 1000000; i--) {
    System.out.println("Hello");
}

答案 8 :(得分:1)

您在这里看到的几乎肯定是HotSpot内联的不同之处。使用更简单的循环,它更可能内联,因此摆脱了所有多余的垃圾。它可能会执行相同的内联,但可以更早或更省力地完成。通常使用Java微基准测试,您应该多次运行代码,从中可以计算出启动时间,平均时间和偏差。

答案 9 :(得分:0)

在“编译器优化”的情况下,您可以做的最好是每个循环:

for(final String x : myList) { ... }

让编译器提供最快的实现。

编辑:

代码示例之间的区别在于for循环的第二个参数。在第一个示例中,VM将执行方法调用(更昂贵)并因此更慢(仅在存在大量迭代时才显着)。在您的第二个示例中,VM将执行堆栈弹出(更便宜,并且局部变量在堆栈上),因此更快(仅在存在大量迭代时才显着:对于仅一次迭代,第一次迭代更快,在内存使用方面)。

另外:“过早优化是所有邪恶的根源。”唐纳德克努特的臭名昭着的法律。

答案 10 :(得分:0)

不同之处在于每次迭代都会减少一个方法调用,因此第二个版本应该运行得稍快一些。虽然如果你使用Just-In-Time编译器,他可以优化它 - 确定它在循环期间不会改变。标准Java实现以JIT为特色,但不是每个Java实现都有。

答案 11 :(得分:0)

与往常一样,你将不得不运行它们,看看哪个更快,因为你正在使用的实现。但是,第一个具有必须在每次迭代时调用size()的潜在性能损失,并且函数调用比直接检查变量更昂贵。但是,根据您的代码和编译器的功能,可能会优化该函数调用,因此您必须运行测试才能看到。

然而,正如Pindatjuh所指出的那样,当你要像这样迭代整个集合时,最好使用foreach循环。它应该让编译器更好地优化事物并且不易出错。

答案 12 :(得分:0)

使用最后一个示例,您不需要解析数组的当前大小,因此它将比第一个示例稍快一些。

请记住,只有在不更改数组中的值数时,这才有用。

在Android中,建议使用示例中的最新示例Designing for Performance。 http://developer.android.com/guide/practices/design/performance.html#foreach