下面的代码分别调用两个简单的函数100亿次。
public class PerfTest {
private static long l = 0;
public static void main(String[] args) {
List<String> list = Arrays.asList("a", "b");
long time1 = System.currentTimeMillis();
for (long i = 0; i < 1E10; i++) {
func1("a", "b");
}
long time2 = System.currentTimeMillis();
for (long i = 0; i < 1E10; i++) {
func2(list);
}
System.out.println((time2 - time1) + "/" + (System.currentTimeMillis() - time2));
}
private static void func1(String s1, String s2) { l++; }
private static void func2(List<String> sl) { l++; }
}
我的假设是这两个电话的表现几乎相同。如果有什么我会猜到传递两个参数会比传递一个稍慢。鉴于所有参数都是对象引用,我并不期望有一个列表可以产生任何差异。
我已经多次运行测试,典型的结果是&#34; 12781/30536&#34;。换句话说,使用两个字符串的呼叫需要13秒,使用列表的呼叫需要30秒。
这种性能差异的解释是什么?或者这是不公平的考验?我已经尝试切换两个调用(如果它是由于启动效果)但结果是相同的。
更新
由于许多原因,这不是一个公平的测试。但它确实展示了Java编译器的真实行为。请注意以下两个补充说明:
s1.getClass()
和sl.getClass()
会使两个函数调用相同-XX:-TieredCompilation
运行测试也会使两个函数调用执行相同的此行为的解释在下面接受的答案中。 @ apangin的答案非常简短的总结是热点编译器没有内联func2
,因为它的参数类(即List
)没有被解析。强制类的解析(例如使用getClass
)会使其内联,从而显着提高其性能。正如答案中指出的那样,未解决的类不太可能出现在实际代码中,这使得这段代码成为不切实际的边缘情况。
答案 0 :(得分:19)
基准是unfair,然而,它显示了一个有趣的效果。
正如Sotirios Delimanolis所注意到的,性能差异是由于HotSpot编译器内联func1
,而func2
没有内联。原因是类型为func2
的{{1}}参数,该类在执行基准测试期间从未被解析。
请注意,List
类实际上并未使用:没有调用List方法,没有声明List类型的字段,没有类型强制转换,也没有执行其他通常会导致类{{3 }}。如果您在代码中的任何位置添加List
类的使用,则会{}}内联{。}}。
影响编译策略的另一个环节是方法的简单性。这很简单,JVM决定在第1层编译它(C1没有进一步的优化)。如果使用C2编译,则List
类将被解析。尝试使用func2
投放,并且您会看到List
已成功内联,并且执行速度与-XX:-TieredCompilation
一样快。
手动编写逼真的微基准测试是一项非常困难的工作。有许多方面可能导致令人困惑的结果,例如内联,死代码替换,堆栈替换,配置文件污染,重新编译等。这就是为什么强烈建议使用适当的基准测试工具,如resolution。手写的基准测试很容易欺骗JVM。特别是,真正的应用程序不太可能拥有从未使用过的类的方法。