我们都去过那儿。 在查看设计不良的代码的同时,听听作者对人们永远不应该牺牲性能而不是设计的解释。 而且,您不能说服作者摆脱其500行方法,因为链接方法调用会破坏性能。
好吧,这可能在1996年左右是正确的。 但是自那时以来, JVM已经发展成为一款了不起的软件。 找出它的一种方法是开始更深入地研究虚拟机执行的优化。 JVM应用的技术库非常广泛,但是让我们更详细地研究其中一种。 即方法内联 。 通过以下示例最容易解释:
int sum(int a, int b, int c, int d) {return sum(sum(a, b),sum(c, d));
}int sum(int a, int b) {return a + b;
}
当运行此代码时, JVM将弄清楚它可以用更有效的所谓“内联”代码代替:
int sum(int a, int b, int c, int d) {return a + b + c + d;
}
您必须注意,此优化是由虚拟机而不是由编译器完成的。 最初做出此决定的原因并不透明。 毕竟-如果您查看上面的示例代码-为什么在编译时可以推迟优化以产生更有效的字节码? 但是,考虑到其他不太明显的情况,JVM是执行优化的最佳位置:
- JVM除了静态分析外还配备了运行时数据。 在运行时,JVM可以根据最常执行的方法,冗余的负载,何时安全使用副本传播等做出更好的决策。
- JVM已获得有关基础体系结构的信息-内核数,堆大小和配置,因此可以根据此信息进行最佳选择。
但是,让我们在实践中看到这些假设。 我创建了一个小型测试应用程序 ,它使用几种不同的方法将1024个整数加在一起。
- 相对合理的一种,其中实现只是对包含1024个整数的数组进行迭代,并将结果求和。 InlineSummarizer.java中提供了此实现。
- 基于递归的分而治之方法。 我采用原始的1024个元素数组,然后将其递归地分成两半,因此第一个递归深度为我提供了两个512个元素数组,第二个深度为4个256个元素数组,依此类推。 为了将所有1024个元素加在一起,我引入了1023个其他方法调用。 此实现作为RecursiveSummarizer.java附加。
- 天真的分而治之的方法。 这也划分了原始的1024个元素的数组,但是通过在分开的两半上调用其他实例方法-即我嵌套了sum512(),sum256(),sum128(),…,sum2()调用,直到我总结了所有元素。 与递归一样,我在源代码中介绍了1023个其他方法调用。
我有一个测试班来运行所有这些示例。 最初的结果来自未优化的代码:
从上面可以看出,内联代码是最快的。 而我们引入了1023个其他方法调用的方法则要慢25,000ns。 但是此图像必须加以警告-它是JIT尚未完全优化代码的运行快照。 在我2010年中期的MB Pro中,根据实施情况,运行了200到3000次。
更现实的结果如下。 我已经运行了所有汇总器实现超过1,000,000次,并丢弃了JIT尚未成功实现魔术效果的运行。
我们可以看到,即使内联代码仍然表现最佳,但迭代方法也以相当快的速度飞速发展。 但是递归明显不同–当迭代方法仅以20%的开销关闭时,RecursiveSummarizer会花费内联代码需要完成的340%的时间。 显然,这是应该注意的事情–使用递归时,JVM是无助的,无法内联方法调用。 因此,在使用递归时请注意此限制。
除了递归-方法开销几乎不存在。 源代码中有1023个其他方法调用之间的时间差仅为205 ns。 请记住,那是我们用于测量的那纳秒(10 ^ -9 s)。 因此,借助JIT,我们可以安全地忽略方法调用带来的大部分开销。 下次当您的同事将笨拙的设计决策隐藏在通过调用堆栈弹出效率不高的语句后面时,请让他首先完成一个小的JIT崩溃过程 。 如果您希望有能力阻止他将来的荒唐言论,请订阅我们的RSS或Twitter提要,我们很高兴为您提供未来的案例研究。
全面披露:本文所用测试用例的灵感来自Tomasz Nurkiewicz 博客文章 。
参考: Plumbr Blog博客上的JCG合作伙伴 Nikita Salnikov Tarnovski 用Java进行方法调用的成本是多少 ?
翻译自: https://www.javacodegeeks.com/2013/02/how-expensive-is-a-method-call-in-java.html