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

spr*_*ter 15 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++; }
}
Run Code Online (Sandbox Code Playgroud)

我的假设是这两个电话的表现几乎相同.如果有什么我会猜到传递两个参数会比传递一个稍慢.鉴于所有参数都是对象引用,我并不期望有一个列表可以产生任何差异.

我已多次运行测试,典型结果为"12781/30536".换句话说,使用两个字符串的呼叫需要13秒,使用列表的呼叫需要30秒.

这种性能差异的解释是什么?或者这是不公平的考验?我已经尝试切换两个调用(如果它是由于启动效果)但结果是相同的.

更新

由于许多原因,这不是一个公平的测试.但它确实展示了Java编译器的真实行为.请注意以下两个补充说明:

  • 添加表达式s1.getClass()sl.getClass()函数使两个函数调用相同
  • 运行测试 -XX:-TieredCompilation也会使两个函数调用执行相同的操作

对此行为的解释在下面接受的答案中.@ apangin的答案的简短摘要func2是热点编译器没有内联,因为它的参数类(即List)没有被解析.强制类的解析(例如使用getClass)会使其内联,从而显着提高其性能.正如答案中指出的那样,未解决的类不太可能出现在实际代码中,这使得此代码成为不切实际的边缘情况.

apa*_*gin 19

基准是不公平的,然而,它揭示了一个有趣的效果.

正如Sotirios Delimanolis所注意到的,性能差异是func1由HotSpot编译器内联的事实造成的,而func2不是.原因是func2类型的参数,在执行基准测试期间List从未解决过的类.

请注意,List实际上并未使用类:没有调用List方法,没有声明List类型的字段,没有类强制转换,也没有执行通常会导致类解析的其他操作.如果List在代码中的任何位置添加类的使用,func2则将内联.

影响编译策略的另一个环节是方法的简单性.这很简单,JVM决定在第1层编译它(C1没有进一步的优化).如果它是用C2编译的,那么List类将被解析.尝试运行-XX:-TieredCompilation,您将看到func2成功内联,并执行速度最快func1.

手动编写逼真的微基准测试是一项非常困难的工作.有很多方面可能导致令人困惑的结果,例如内联,死代码消除,堆栈替换,配置文件污染,重新编译等.这就是为什么强烈建议使用适当的基准测试工具,如JMH.手写的基准测试很容易欺骗JVM.特别是,真正的应用程序不太可能拥有从未使用过的类的方法.