【问题标题】:Why is passing two string arguments more efficient than one list argument为什么传递两个字符串参数比一个列表参数更有效
【发布时间】:2016-10-20 22:50:48
【问题描述】:

下面的代码分别调用了两个简单的函数 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++; }
}

我的假设是这两个调用的性能将接近相同。如果有的话,我会猜到传递两个参数会比传递一个稍慢。鉴于所有参数都是对象引用,我没想到一个列表会产生任何影响。

我已经多次运行测试,典型的结果是“12781/30536”。换句话说,使用两个字符串的调用需要 13 秒,使用列表的调用需要 30 秒。

对这种性能差异的解释是什么?或者这是一个不公平的测试?我试过切换这两个调用(以防它是由于启动影响),但结果是一样的。

更新

这不是一个公平的测试,原因有很多。然而,它确实展示了 Java 编译器的真实行为。请注意以下两个添加内容来证明这一点:

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

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

【问题讨论】:

  • 您能添加您期望的内容吗?为什么?
  • @ChiefTwoPencils 在上面添加了一个段落。
  • 我没有投票关闭,但除非有人愿意破解运行时以查看特定的编译优化,否则大多数性能问题并不是很有用(尽管它们可能很有趣/有趣)——答案可能会因版本而异。在这种情况下,我只是假设 JVM 发现编译或记住两个参数调用比数组调用更容易,但说真的——只写最易读的东西!另请注意,可读性最高的版本通常是 JVM 优化得最好的版本。
  • @BillK 我完全同意代码的清晰度比性能更重要。但我很好奇为什么两者之间存在如此显着的差异,我发帖是因为我希望过去有人对此进行过调查并做出解释。
  • ps。对于此调用: func1("a", "b"); java 可能不会传递任何东西,实际上它可能会内联整个东西并只返回一个常量。 Java 在运行时优化的能力使得编写一个好的性能测试非常困难(这本身就是一个很好的迹象,说明为什么 java 的性能如此出色)

标签: java performance


【解决方案1】:

基准是unfair,但它显示了一个有趣的效果。

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

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

影响编译策略的另一个因素是方法的简单性。它是如此简单,以至于 JVM 决定在第 1 层(没有进一步优化的 C1)中编译它。如果用 C2 编译,List 类将被解析。尝试使用-XX:-TieredCompilation 运行,您会看到func2 已成功内联,并且执行速度与func1 一样快。

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

【讨论】:

  • 我尝试向这两个函数添加代码以确保 List 得到解析,并尝试使用 TieredCompliation 选项。正如您所预测的那样,两者都使func2 的性能与func1 一样快。感谢您的明确解释。我会更新问题,以便让未来的读者清楚了解这些信息。
  • 自从提出这个问题以来,我已经对微基准进行了相当多的研究,并按照stackoverflow.com/questions/504103/… 的建议尝试了 JMH 和手工编码。经过几次尝试后,我得出的结论是,只要您不想要精确度并且测试时间足够长,基于秒表的测试就会得出完全相同的结论。在您看来,所有对公平测试的焦虑是否真的有道理,还是悬而未决?
  • @sprinter 我有很多个例子,当原始的基于秒表的基准测试产生误导性结果时。以下是来自 SO 的一些示例:123456789
  • 另一个关于common benchmarking pitfalls的好读物
  • 基准分数本身是没有意义的,即使是通过一个好的工具来衡量。只有当这些分数得到解释时,才能得出结论。 JMH 还有助于解释分数,例如通过显示热点、程序集列表、GC 统计信息等。例如,在这个问题中,它帮助我发现 func1 是由 C2 编译并内联的,而 func2 是由 C1 编译的。
猜你喜欢
  • 1970-01-01
  • 2012-07-17
  • 1970-01-01
  • 2021-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多