考虑这个例子:
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数组),我可以认为他会这样做。
答案 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