背景
大家都知道,在写循环语句时有个优化技巧就是将条件计算尽量放到循环外部,避免每次循环时触发不必要的调用和计算,比如将
for (int i = 0; i < s.length(); i++) {
// ....
}
优化为:
int len = s.length();
for (int i = 0; i < len; i++) {
// ....
}
问题
有了以上的优化技巧, 你对下面代码的执行结果预期是 innerLen
更快还是 outerLen
更快呢?
反汇编
因为 JVM 的强大,很多时候我们都会觉得:“是不是编译器偷偷做了什么优化导致结果反常?”带着这个疑问,我们就先看下字节码文件反汇编出来的结果:
C:\Users\s\Documents\NetBeansProjects\Test\build\classes>javap -c test\Loop.class
Compiled from "Loop.java"
public class test.Loop {
public test.Loop();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
略过
public static void outerLen(java.lang.String);
Code:
0: new #19 // class java/util/HashSet
3: dup
4: invokespecial #20 // Method java/util/HashSet."<init>":()V
7: astore_1
8: aload_0
9: invokevirtual #9 // Method java/lang/String.length:()I
12: istore_2 // len 结果保存在变量 2 中
13: iconst_0 // 将整型值 0 入栈
14: istore_3 // 出栈并存入变量 3 中
15: iload_3 // 载入变量 3(循环开始,i = 0)
16: iload_2 // 载入变量 2,即 len
17: if_icmpge 35 // 变量 3 大于等于变量 2 的话跳到 35(结束循环)
20: aload_1
21: ldc #21 // String i
23: invokeinterface #22, 2 // InterfaceMethod java/util/Set.add:(Ljava/lang/Object;)Z
28: pop
29: iinc 3, 1 // 变量 3 自加 1
32: goto 15
35: return
public static void innerLen(java.lang.String);
Code:
0: new #19 // class java/util/HashSet
3: dup
4: invokespecial #20 // Method java/util/HashSet."<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: aload_0
12: invokevirtual #9 // Method java/lang/String.length:()I
15: if_icmpge 33
18: aload_1
19: ldc #21 // String i
21: invokeinterface #22, 2 // InterfaceMethod java/util/Set.add:(Ljava/lang/Object;)Z
26: pop
27: iinc 2, 1
30: goto 10
33: return
}
outerLen
的 15-32 是循环部分,innerLen
的 10-30 是循环部分。我们可以清晰看到 innerLen
在循环中调用了 String#length() 方法,所以理论上它的耗时是会更多的。
那为什么还会出现 innerLen
耗时更短的现象呢?
更多的测试
编译器永远是对的,代码也没看出来问题,那就是测试方式不对了?
加入多种测试方式并记录结果:
-
重复 10 次运行,发现全部都是 innerLen
快
-
通过调换 outerLen
和 innerLen
执行顺序(让 outerLen
先跑),我们发现结果逆转了,outerLen
终于符合预期,更快了
-
同一次运行中加入重复次数
for (int i = 0; i < 10; i++) {
System.out.print("inner: ");
long startInner = System.currentTimeMillis();
innerLen(str);
System.out.println(System.currentTimeMillis() - startInner);
System.out.print("outer: ");
long startOuter = System.currentTimeMillis();
outerLen(str);
System.out.println(System.currentTimeMillis() - startOuter);
System.out.println("----");
}
发现除了第一次是 innerLen
快,其余 9 次均是 outerLen
快,这 9 次符合预期
看来问题是因为 JVM 某种“动态”因素决定的,这个因素很可能和 GC 有关。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于