java方法调用有多贵
Posted
技术标签:
【中文标题】java方法调用有多贵【英文标题】:java how expensive is a method call 【发布时间】:2011-09-23 14:44:21 【问题描述】:我是一个初学者,我一直认为重复代码是不好的。但是,似乎为了不这样做,您通常必须进行额外的方法调用。假设我有以下课程
public class BinarySearchTree<E extends Comparable<E>>
private BinaryTree<E> root;
private final BinaryTree<E> EMPTY = new BinaryTree<E>();
private int count;
private Comparator<E> ordering;
public BinarySearchTree(Comparator<E> order)
ordering = order;
clear();
public void clear()
root = EMPTY;
count = 0;
将 clear() 方法中的两行复制并粘贴到构造函数中而不是调用实际方法对我来说是否更理想?如果是这样,它有多大的不同?如果我的构造函数进行了 10 次方法调用,每一次都只是将一个实例变量设置为一个值怎么办?最佳编程实践是什么?
【问题讨论】:
但是等等,如果您现在调用该方法,我们将抛出一个 SECOND 方法调用,完全免费!只需支付运费和手续费!不过说真的。方法调用有开销,就像加载更多代码有开销一样。在某些时候,一个变得比另一个更昂贵。唯一的判断方法是对您的代码进行基准测试。 过早的优化报价在 3 ... 2 ... 1 我看不出对此投反对票的原因 - 这家伙问的是一个完全合理的问题。对某些人来说,这可能是一个显而易见的答案,但这并不是一个坏问题! 确实,这是一个非常合理的问题,如果有完全相同的副本,可能会否决票。 是的,如果很明显很抱歉,但我正在自学,而且我只学了几个月。我在网上的示例代码中看到的一些东西我发现不是很好的做法,所以我只想仔细检查一下。 【参考方案1】:将我的 clear() 方法中的两行复制并粘贴到构造函数中而不是调用实际方法对我来说是否更理想?
编译器可以执行该优化。 JVM 也可以。编译器作者和 JVM 作者使用的术语是“内联扩展”。
如果是这样,它有多大的不同?
测量它。通常,您会发现它没有任何区别。如果你认为这是一个性能热点,那你就找错地方了;这就是为什么你需要测量它。
如果我的构造函数调用了 10 次方法调用,每一次都只是简单地将一个实例变量设置为一个值?
同样,这取决于生成的字节码和 Java 虚拟机执行的任何运行时优化。如果编译器/JVM 可以内联方法调用,它将执行优化以避免在运行时创建新的堆栈帧的开销。
最佳编程实践是什么?
避免过早优化。最佳做法是编写可读且设计良好的代码,然后针对应用程序中的性能热点进行优化。
【讨论】:
什么是基准测试的好方法?有没有我可以下载的软件,或者你的意思是在开始和结束时使用 System.nanoTime() 并打印差异?System.nanoTime()
或 System.currentTimeMillis
是一种糟糕的分析方式。您可以从this *** question 的答案中获取分析器列表。我会推荐 VisualVM,因为它现在带有 JDK。
@jhlu87:我认为您很难准确估计方法调用的开销。微基准测试很难做到正确,即使这样,在宏观计划中通常也不是很有用。阅读this。
@VineetReynolds 链接的问题已死。【参考方案2】:
其他人所说的优化是绝对正确的。
从性能的角度来看没有理由内联该方法。如果是性能问题,JVM 中的 JIT 将内联它。在 java 中,方法调用非常接近免费,不值得考虑。
话虽如此,这里还有一个不同的问题。也就是说,从构造函数调用可覆盖的方法(即不是final
、static
或private
)是一种不好的编程习惯。 (Effective Java,第 2 版,第 89 页,标题为“设计和文档以进行继承或禁止继承”)
如果有人添加了一个名为 LoggingBinarySearchTree
的 BinarySearchTree
的子类,该子类会使用以下代码覆盖所有公共方法:
public void clear()
this.callLog.addCall("clear");
super.clear();
那么LoggingBinarySearchTree
将永远无法构建!问题是当BinarySearchTree
构造函数运行时this.callLog
将是null
,但是被调用的clear
是被覆盖的,你会得到一个NullPointerException
。
请注意,Java 和 C++ 在这里有所不同:在 C++ 中,调用 virtual
方法的超类构造函数最终会调用超类中定义的构造函数,而不是被覆盖的构造函数。在两种语言之间切换的人有时会忘记这一点。
鉴于此,我认为在您的情况下,内联 clear
方法在从构造函数调用时可能更干净,但通常在 Java 中,您应该继续让所有方法调用您想要。
【讨论】:
我认为他不是在询问编码风格的技巧,而是想知道方法调用是否昂贵 他明确地问“最好的编程实践是什么?” - 作为最佳实践,这是完全相关的。 如果你只取最后一句话,你就完全失去了这个问题的上下文。他想知道将大方法分解成许多小方法是否成本高昂,因为方法调用是有代价的。添加一个描述用于从构造函数调用非最终方法的反模式的答案并不能算作对他的整个问题的答案。哦,看看问题的标题“方法调用有多贵”【参考方案3】:我肯定会保持原样。如果更改clear()
逻辑会怎样?找到你复制了 2 行代码的所有地方是不切实际的。
【讨论】:
【参考方案4】:一般来说(作为初学者,这意味着永远!)您永远不应该像您正在考虑的那样进行微优化。总是喜欢代码的可读性而不是这样的事情。
为什么?因为编译器/热点会即时为您进行这些优化,还有很多很多。如果有的话,当您尝试按照这些方式进行优化时(尽管在这种情况下不是),您可能会使事情变慢。 Hotspot 了解常见的编程习惯,如果您尝试自己进行优化,它可能无法理解您要执行的操作,因此无法对其进行优化。
还有更高的维护成本。如果您开始重复代码,那么维护起来会更加困难,这可能比您想象的要麻烦得多!
顺便说一句,您可能会在编码生涯中遇到一些需要进行低级优化的点 - 但如果您达到了这些点,那么您肯定会知道时机成熟。如果您不这样做,您可以随时返回并在需要时进行优化。
【讨论】:
【参考方案5】:最佳做法是测量两次并切割一次。
一旦您浪费了优化时间,就再也无法找回! (所以先测量一下,然后问问自己是否值得优化。你会节省多少实际时间?)
在这种情况下,Java VM 可能已经在进行您所说的优化。
【讨论】:
【参考方案6】:方法调用的成本是堆栈帧的创建(和处置)以及一些额外的字节码表达式(如果您需要将值传递给方法)。
【讨论】:
【参考方案7】:我遵循的模式是,该方法是否满足以下条件之一:
让这个方法在这个类之外可用会有帮助吗? 将此方法用于其他方法会有帮助吗? 每次需要时都重写它会令人沮丧吗? 是否可以通过使用几个参数来增加该方法的多功能性?如果以上任何一个是真的,它应该被包裹在它自己的方法中。
【讨论】:
不问这些问题更容易,只要把该死的代码放在它自己的方法中! 提问者很好奇他的方法调用的粒度应该是多少。如果您可以使用i++;
,则无需创建递增整数的方法
实际上,创建一个方法有很多价值,即使它没有机会被重用。只需给代码块起个名字,让整体结构出现,这本身就是一个很大的好处。【参考方案8】:
保持clear()
方法有助于提高可读性。拥有不可维护的代码更昂贵。
【讨论】:
【参考方案9】:优化编译器通常可以很好地消除这些“额外”操作的冗余;在许多情况下,“优化”代码和简单地按照您想要的方式编写并通过优化编译器运行的代码之间的区别是没有的;也就是说,优化编译器通常会做的和你做的一样好,而且它不会导致源代码的任何退化。事实上,很多时候,“手动优化”的代码最终效率较低,因为编译器在进行优化时会考虑很多事情。将您的代码保留为可读格式,然后再担心优化问题。
“过早的优化是 都是邪恶的。”——唐纳德·高德纳
【讨论】:
【参考方案10】:我不会担心方法调用,而是方法的逻辑。如果它是关键系统,并且系统需要“快速”,那么我会考虑优化需要很长时间才能执行的代码。
【讨论】:
【参考方案11】:考虑到现代计算机的内存,这非常便宜。将代码分解为方法总是更好,这样人们就可以快速阅读发生了什么。如果错误仅限于具有几行正文的单个方法,它还有助于缩小代码中的错误范围。
【讨论】:
【参考方案12】:正如其他人所说,方法调用的成本是微不足道的,因为编译器会为你优化它。
也就是说,从构造函数调用实例方法是有危险的。您冒着稍后更新实例方法的风险,以便它可能会尝试使用尚未由构造函数启动的实例变量。也就是说,您不一定要从构造函数中分离出构造活动。
另一个问题——您的 clear() 方法将根设置为 EMPTY,它在创建对象时被初始化。如果您随后将节点添加到 EMPTY,然后调用 clear(),您将不会重置根节点。这是你想要的行为吗?
【讨论】:
以上是关于java方法调用有多贵的主要内容,如果未能解决你的问题,请参考以下文章