java方法调用有多昂贵


81

我是一个初学者,我一直读到重复代码很不好。但是,似乎要避免这样做,通常必须进行额外的方法调用。假设我有以下课程

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次方法调用,该怎么办?什么是最佳编程实践?


4
但是,等等,如果您现在调用该方法,我们将完全免费地进行SECOND方法调用!只支付运输和处理!说真的,不过。方法调用有开销,就像有更多代码要加载一样有开销。在某个时候,一个变得比另一个更昂贵。唯一的方法就是对代码进行基准测试。
Marc B

11
3 ... 2 ... 1中的过早优化报价

21
我看不出对此否决的原因-这个人在问一个完全合法的问题。对于某些人来说,这可能是一个显而易见的答案,但这并不是一个坏问题!
Michael Berry

3
的确,这是一个非常合理的问题,只有当有完全相同的副本时,才可以投下反对票的唯一原因是。
Arafangion 2011年

1
是的,如果这很明显,很抱歉,但是我正在自己学习,而且我只从事了几个月。我发现在网上示例代码中看到的一些东西不是很好的做法,所以我只想仔细检查一下。
jhlu87 2011年

Answers:


74

对于我来说,将我的clear()方法中的两行复制并粘贴到构造函数中而不是调用实际方法会更好吗?

编译器可以执行该优化。JVM也可以。编译器编写者和JVM作者使用的术语是“内联扩展”。

如果是这样,有什么不同?

测量它。通常,您会发现这没有什么区别。而且,如果您认为这是性能热点,那么您就在错误的地方寻找;这就是为什么您需要测量它。

如果我的构造函数仅通过将实例变量设置为一个值就进行了10次方法调用,该怎么办?

同样,这取决于生成的字节码和Java虚拟机执行的任何运行时优化。如果编译器/ JVM可以内联方法调用,它将执行优化以避免运行时创建新堆栈帧的开销。

什么是最佳编程实践?

避免过早的优化。最佳实践是编写可读性和设计良好的代码,然后针对应用程序中的性能热点进行优化。


基准测试的好方法是什么?有什么我可以下载的软件,或者您是说仅在开始和结束时使用System.nanoTime()并打印出区别?
jhlu87 2011年

System.nanoTime()System.currentTimeMillis做剖析的方法很差。您可以从有关Stackoverflow问题的答案中获取探查器列表。我建议使用VisualVM,因为它随JDK一起提供。
Vineet Reynolds

2
@ jhlu87:我认为您很难准确估计方法调用的开销。微基准测试很难正确实现,即使那样,在总体方案中也并非十分有用。阅读
ColinD 2011年

@VineetReynolds链接的问题已死。
dim8

19

其他人都说过的优化是绝对正确的。

从性能的角度来看,没有理由内联该方法。如果是性能问题,则JVM中的JIT将对其进行内联。在Java中,方法调用几乎是免费的,因此不值得考虑。

话虽这么说,这里有一个不同的问题。即,它不好的编程习惯调用一个方法覆写投放(即,一个是不finalstaticprivate从构造器)。(有效的Java,第二版,第89页,标题为“继承的设计和文档或禁止继承”的项目)

如果有人添加一个BinarySearchTree名为的子类,该子类LoggingBinarySearchTree使用以下代码覆盖所有公共方法,会发生什么情况:

public void clear(){
  this.callLog.addCall("clear");
  super.clear();
}

然后,LoggingBinarySearchTree将永远无法建设!问题是,this.callLognull在当BinarySearchTree构造函数运行,但clear被调用是重写一个,你会得到一个NullPointerException

请注意,Java和C ++在这里有所不同:在C ++中,调用virtual方法的超类构造函数最终将调用超类中定义的方法,而不是被覆盖的方法。人们在两种语言之间切换有时会忘记这一点。

鉴于此,我认为从构造函数调用时内联该clear方法可能更干净,但通常在Java中,应继续进行所需的所有方法调用。


1
我不认为他是在询问编码风格的技巧,而是想知道方法调用是否昂贵
Asaf Mesika

3
他明确地问:“最佳编程实践是什么?” -作为最佳做法,这是完全相关的。
Daniel Martin

2
如果仅听完最后一句话,您将完全失去此问题的上下文。他想知道将大型方法分解为许多较小的方法是否昂贵,因为方法调用需要付出一定的代价。添加一个描述用于从构造函数调用非最终方法的反模式的答案,这并不算是对他的问题的整体答案。哦,看看“方法调用有多昂贵”这个问题的标题
Asaf Mesika 2015年

6

我一定会保留原样。如果您更改clear()逻辑该怎么办?找到复制两行代码的所有位置是不切实际的。


4

一般而言(对于初学者来说,这总是意味着!),您永远不应像您正在考虑的那样进行微优化。始终喜欢代码的可读性,而不是像这样的事情。

为什么?因为编译器/热点将即时为您进行这些优化,还有很多很多。如果有的话,当您尝试按照这些方式进行优化时(虽然不是在这种情况下),您可能会使速度变慢。Hotspot理解常见的编程习惯用法,如果您自己尝试进行优化,则可能无法理解要执行的操作,因此无法对其进行优化。

维护成本也更高。如果您开始重复代码,那么维护起来会付出更多的努力,这可能比您想象的要麻烦得多!

顺便说一句,您可能会在编码过程中遇到一些需要做低级优化的地方-但是,如果您达到了这些点,您肯定会知道何时到了。如果没有,您可以随时返回并在以后进行优化(如果需要)。


3

最佳做法是测量两次,然后切割一次。

一旦浪费了时间优化,就再也找不回来!(因此,请先进行测量,然后问问自己是否值得优化。您将节省多少实际时间?)

在这种情况下,Java VM可能已经在进行您正在谈论的优化。



1

我遵循的模式是所讨论的方法是否满足以下条件之一:

  • 在此类之外提供此方法是否有帮助?
  • 将此方法用于其他方法会有所帮助吗?
  • 每次我需要重写它都会感到沮丧吗?
  • 使用一些参数是否可以增加方法的多功能性?

如果以上任何一项正确,则应使用自己的方法将其包装起来。


不问这些问题,而已将织补代码放入它自己的方法中就更容易了!
Arafangion 2011年

2
询问者很好奇他的方法调用的粒度。如果您可以使用,则无需创建增加整数的方法i++;
Peaches491'6

实际上,即使没有机会被重用,创建一个方法也有很多价值。仅仅给一个代码块起一个名字并使整个结构看起来本身就是一个很大的好处。
乔佛里


1

优化编译器通常可以很好地消除这些“额外”操作的冗余。在许多情况下,“优化的”代码与通过所需的简单方式编写并通过优化的编译器运行的代码之间没有区别;也就是说,优化编译器通常会像您所做的一样出色地完成工作,并且不会造成源代码的任何降级。实际上,很多时候,“手工优化”的代码最终都是效率极低的,因为编译器在进行优化时会考虑很多事情。将代码保留为可读的格式,直到以后再担心优化。

“过早的优化是万恶之源。” -唐纳德​​·努斯


0

我不必担心方法调用,而是方法的逻辑。如果这是关键系统,并且系统需要“快速”运行,那么我将考虑优化执行时间长的代码。


0

考虑到现代计算机的内存,这是非常便宜的。将您的代码分解为方法总是更好,以便有人可以快速阅读正在发生的事情。如果错误仅限于一行几行的单个方法,则也将有助于缩小代码中的错误。


0

就像其他人所说的那样,方法调用的成本是微不足道的,因为编译器会为您优化它。

也就是说,从构造函数对实例方法进行方法调用存在危险。您冒着以后更新实例方法的风险,以便它可能尝试使用构造函数尚未启动的实例变量。也就是说,您不一定要从构造函数中分离出构造活动。

另一个问题-您的clear()方法将根设置为EMPTY,该名称在创建对象时初始化。如果随后将节点添加到EMPTY,然后调用clear(),则不会重置根节点。这是您想要的行为吗?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.