为什么传递两个字符串参数比一个列表参数

时间:2016-10-20 22:50:48

标签: java performance

下面的代码分别调用两个简单的函数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)会使其内联,从而显着提高其性能。正如答案中指出的那样,未解决的类不太可能出现在实际代码中,这使得这段代码成为不切实际的边缘情况。

1 个答案:

答案 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。特别是,真正的应用程序不太可能拥有从未使用过的类的方法。