Java方法调用的成本有多高?

88

我是一名初学者,我一直听说重复代码是不好的。但是,为了避免这种情况,通常需要进行额外的方法调用。假设我有以下类:

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
等等,如果你现在调用该方法,我们将额外提供第二个方法调用,完全免费!只需支付运输和处理费用!但是,调用方法会产生开销,就像加载更多代码一样也会产生开销。在某个点上,一个比另一个更加昂贵。唯一的判断方法是通过对代码进行基准测试。 - Marc B
13
过早优化的引用语:3... 2... 1... - user177800
30
我不明白为什么会有人对这个问题点踩,这位用户所问的是非常合理的。对于一些人来说,这可能是一个显而易见的答案,但这并不意味着这是一个糟糕的问题! - Michael Berry
3
确实,这是一个非常合理的问题,唯一导致反对票的原因可能是存在完全相同的副本。 - Arafangion
1
是的,很抱歉如果这显而易见,但我是自学的,而且我只学了几个月。我在网上看到一些示例代码中发现有一些不好的实践方式,所以我想再确认一下。 - jhlu87
显示剩余4条评论
12个回答

81

将两行代码直接复制到构造函数中,而不是调用clear()方法,这样是否更优?

编译器可以进行这种优化。JVM也能做到这一点。编译器作者和JVM开发者使用的术语是“内联扩展”。

如果是这样的话,这会有多大的区别?

测量一下吧。通常情况下,你会发现这并没有什么区别。如果你认为这是一个性能热点,那么你就在错误的地方寻找问题;所以你需要对其进行测量。

如果我的构造函数调用了10个方法,每个方法只是将实例变量设置为一个值呢?

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

最佳的编程实践是什么?

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


什么是好的基准测试方法?有没有可以下载的软件,或者你是指在开始和结束时使用System.nanoTime()并打印差异? - jhlu87
System.nanoTime()System.currentTimeMillis是一种不好的性能分析方法。你可以在这个Stackoverflow问题的答案中找到一系列的性能分析工具。我推荐使用VisualVM,因为它现在已经随JDK一起提供了。 - Vineet Reynolds
2
@jhlu87:我认为你很难准确估计方法调用的开销。微基准测试很难做到正确,即使这样做通常在大局上也没有太大用处。阅读这篇文章 - ColinD
@VineetReynolds,链接的问题已经失效。 - Dimitar

20

其他人关于优化的说法是绝对正确的。

从性能角度来看,没有理由内联该方法。如果这是一个性能问题,那么你的JVM中的JIT将会内联它。在Java中,方法调用是如此接近免费,以至于不值得考虑它。

话虽如此,这里有一个不同的问题。也就是说,从构造函数中调用可重写方法(即不是final、static或private的方法)是不好的编程实践。(《Effective Java, 2nd Ed.》第89页,在标题为"Design and document for inheritance or else prohibit it"的项目中)

如果有人添加了一个名为LoggingBinarySearchTreeBinarySearchTree子类,并覆盖了所有公共方法,代码如下:

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

如果这样做,LoggingBinarySearchTree就无法被构造了!问题在于当运行BinarySearchTree构造函数时,this.callLog将是null,但调用的clear是重写的方法,你会得到一个NullPointerException

请注意,Java和C++在这里有所不同:在C++中,调用一个虚方法的超类构造函数最终会调用超类中定义的方法而非重写的方法。有时候在两种语言之间切换的人会忘记这一点。

鉴于此,在您的情况下,我认为在构造函数中内联clear方法 在调用时可能更清晰,但通常情况下,在Java中,您应该直接进行所有方法调用。


2
我认为他并不是在寻求编码风格的建议,而是想知道方法调用是否昂贵。 - Asaf Mesika
3
他明确询问“什么是最佳编程实践?”- 就最佳实践而言,这是完全相关的。 - Daniel Martin
3
如果只看最后一句话,你完全失去了这个问题的背景。他想知道将大方法分解成许多小方法是否昂贵,因为方法调用是有代价的。添加一个描述从构造函数调用非最终方法的反模式的答案,不算作对整个问题的回答。噢,还要看一下问题的标题:“方法调用有多昂贵”。 - Asaf Mesika

7
我肯定会保持原样。如果您更改clear()逻辑怎么办?找到所有复制2行代码的地方是不切实际的。

4
一般来说(对于初学者而言始终如此!),您不应该进行微观优化,比如您正在考虑的那种。始终要优先考虑代码的可读性,而不是像这样的东西。
为什么?因为编译器/热点会在运行时为您执行这些优化,以及许多其他优化。如果您尝试进行这些优化(尽管在这种情况下不是),您可能会使事情变得更慢。热点理解常见的编程习惯,如果您试图自己进行优化,它可能无法理解您尝试做什么,因此无法进行优化。
还有更大的维护成本。如果您开始重复代码,那么维护它将需要更多的工作量,这可能比您想象的麻烦得多!
顺便说一句,您可能会在编码生涯中遇到一些需要进行低级优化的情况-但是如果您达到那些点,您肯定会知道何时需要。如果您没有,可以随时返回并进行优化。

3

最佳实践是量尺寸、再切割。

一旦你浪费时间去优化,就再也无法回到过去了!(所以先量尺寸,问问自己是否值得优化。你会节省多少实际时间?)

在这种情况下,Java虚拟机可能已经在进行你所说的优化。


3

方法调用的成本是创建(和处理)堆栈帧以及一些额外的字节码表达式,如果您需要将值传递给该方法,则会产生这些成本。


1
我遵循的模式是,这个方法是否满足以下条件之一:
  • 将此方法提供给类外部是否有帮助?
  • 将此方法提供给其他方法是否有帮助?
  • 每次需要时都需要重新编写是否令人沮丧?
  • 使用几个参数可以增加该方法的多功能性吗?
如果上述任何条件为真,则应将其封装在自己的方法中。

最好不要问这些问题,直接把该死的代码放到它自己的方法中就行了! - Arafangion
2
提问者想知道他的方法调用应该是什么粒度。如果您可以使用“i ++”来增加整数,则无需创建一个方法来执行此操作。 - Peaches491
实际上,即使永远不会重复使用,创建一个方法也有很大的价值。仅仅给一段代码块命名并使整体结构显现出来就是一个巨大的好处。 - Joffrey

1

在提高可读性的情况下保留clear()方法。难以维护的代码更加昂贵。


1

优化编译器通常能够很好地消除这些“额外”操作中的冗余;在许多情况下,“优化”的代码与仅按照您想要的方式编写并通过优化编译器运行的代码之间的差异是没有的;也就是说,优化编译器通常能够像您一样做得很好,并且它可以在不降低源代码质量的情况下完成。实际上,很多时候,“手动优化”的代码最终效率反而更低,因为编译器在进行优化时会考虑很多因素。将您的代码保持可读性,并在以后再考虑优化。

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


0

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接