Java静态调用比非静态调用更昂贵吗?

112

有没有性能上的好处呢?是编译器/虚拟机特定的吗?我正在使用Hotspot。


可能会有差异,对于任何特定的代码片段,可能会有两种可能的结果,而且可能会随着JVM的微小更新而改变。这绝对是你应该忘记的97%的小效率问题之一 - undefined
12个回答

81

四年后...

好的,为了彻底解决这个问题,我编写了一个基准测试,展示了不同类型的调用(虚拟、非虚拟、静态)之间的比较。

我在ideone上运行了它,这是我的结果:

(迭代次数越多越好。)

    Success time: 3.12 memory: 320576 signal:0
  Name          |  Iterations
    VirtualTest |  128009996
 NonVirtualTest |  301765679
     StaticTest |  352298601
Done.

预料之中的是,虚函数调用最慢,非虚函数调用更快,而静态方法调用甚至更快。

我没有预料到的是差异如此明显:测量结果表明,虚函数调用的速度不到非虚函数调用的一半,而非虚函数调用的速度整体上比静态调用慢15%。这就是这些测量结果显示的内容;实际差异实际上必须稍微更加明显,因为对于每个虚拟、非虚拟和静态方法调用,我的基准测试代码都有额外的常量开销:增加一个整数变量的值、检查布尔变量并循环(如果未满足条件)。

我想结果会因CPU和JVM的不同而异,所以尝试一下,看看您会得到什么:

import java.io.*;

class StaticVsInstanceBenchmark
{
    public static void main( String[] args ) throws Exception
    {
        StaticVsInstanceBenchmark program = new StaticVsInstanceBenchmark();
        program.run();
    }

    static final int DURATION = 1000;

    public void run() throws Exception
    {
        doBenchmark( new VirtualTest( new ClassWithVirtualMethod() ), 
                     new NonVirtualTest( new ClassWithNonVirtualMethod() ), 
                     new StaticTest() );
    }

    void doBenchmark( Test... tests ) throws Exception
    {
        System.out.println( "  Name          |  Iterations" );
        doBenchmark2( devNull, 1, tests ); //warmup
        doBenchmark2( System.out, DURATION, tests );
        System.out.println( "Done." );
    }

    void doBenchmark2( PrintStream printStream, int duration, Test[] tests ) throws Exception
    {
        for( Test test : tests )
        {
            long iterations = runTest( duration, test );
            printStream.printf( "%15s | %10d\n", test.getClass().getSimpleName(), iterations );
        }
    }

    long runTest( int duration, Test test ) throws Exception
    {
        test.terminate = false;
        test.count = 0;
        Thread thread = new Thread( test );
        thread.start();
        Thread.sleep( duration );
        test.terminate = true;
        thread.join();
        return test.count;
    }

    static abstract class Test implements Runnable
    {
        boolean terminate = false;
        long count = 0;
    }

    static class ClassWithStaticStuff
    {
        static int staticDummy;
        static void staticMethod() { staticDummy++; }
    }

    static class StaticTest extends Test
    {
        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                ClassWithStaticStuff.staticMethod();
            }
        }
    }

    static class ClassWithVirtualMethod implements Runnable
    {
        int instanceDummy;
        @Override public void run() { instanceDummy++; }
    }

    static class VirtualTest extends Test
    {
        final Runnable runnable;

        VirtualTest( Runnable runnable )
        {
            this.runnable = runnable;
        }

        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                runnable.run();
            }
        }
    }

    static class ClassWithNonVirtualMethod
    {
        int instanceDummy;
        final void nonVirtualMethod() { instanceDummy++; }
    }

    static class NonVirtualTest extends Test
    {
        final ClassWithNonVirtualMethod objectWithNonVirtualMethod;

        NonVirtualTest( ClassWithNonVirtualMethod objectWithNonVirtualMethod )
        {
            this.objectWithNonVirtualMethod = objectWithNonVirtualMethod;
        }

        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                objectWithNonVirtualMethod.nonVirtualMethod();
            }
        }
    }

    static final PrintStream devNull = new PrintStream( new OutputStream() 
    {
        public void write(int b) {}
    } );
}
值得注意的是,这种性能差异仅适用于除调用无参方法之外的代码。无论在调用之间有什么其他代码,包括参数传递,在其中的差异都会被稀释。实际上,静态和非虚拟调用之间的15%差异可能完全由于不必将“this”指针传递给静态方法而解释。因此,只要有一些简单的代码在调用之间执行,不同类型的调用之间的差异就会被稀释到几乎没有任何影响。
另外,存在虚拟方法调用是有理由的;它们确实有一个目的要服务,并使用底层硬件提供的最有效手段来实现。(CPU指令集。)如果为了通过替换为非虚拟或静态调用而试图消除它们,结果需要添加像 iota 这样的额外代码来模拟它们的功能,那么您的净开销注定不会更少,反而可能会更多。很可能,要多得多,难以想象得多。

11
“Virtual”是C++术语。在Java中没有虚方法(virtual methods)。有普通方法,它们具有运行时多态性(runtime-polymorphic),以及静态或final方法,它们没有这种多态性。 - Zhenya
25
@levgen 是的,对于那些观点像官方高层次语言概述一样狭窄的人来说,正如你所说。但是,当然高级概念是使用早在 Java 存在之前很长时间发明的成熟的低级机制实现的,而虚拟方法就是其中之一。如果你只是稍微看一眼引擎盖下面,你就会立即看到这一点:http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokevirtual - Mike Nakis
20
感谢您回答问题时没有对过早优化做出假设。 很棒的答案。 - vegemite4me
3
是的,那正是我的意思。不管怎样,我刚刚在我的机器上运行了测试。除了你可以预期这样的基准测试会有些抖动外,速度上没有任何差别:VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198 在我的 OpenJDK 安装中。顺便说一句,即使我删除了 final 修饰符,这个结果仍然成立。另外,我必须将 terminate 字段设置为 volatile,否则测试就无法完成。 - Marten
4
在Nexus 5上运行Android 6,我得到了相当惊人的结果:VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170。不仅如此,我的笔记本电脑上的OpenJDK可以执行40倍的迭代,而静态测试始终具有大约30%的较低吞吐量。这可能是ART特定的现象,因为在Android 4.4平板电脑上我得到了预期的结果:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933。 - Marten
显示剩余10条评论

80

首先,您不应该基于性能来选择静态或非静态。

其次,在实践中,这并不会有任何区别。Hotspot可能会选择以使一个方法的静态调用更快,使另一个方法的非静态调用更快的方式进行优化。

第三点:关于静态与非静态的大部分神话都基于非常旧的JVM(它们没有做到Hotspot所做的优化),或者是一些关于C ++的记忆小事(其中动态调用使用比静态调用多一个内存访问)。


1
你说得没错,仅凭这一点就不能偏爱静态方法。然而,如果在某种情况下静态方法适合设计,那么了解它们至少和实例方法一样快,甚至更快是很有用的,不应该因为性能问题而将其排除在外。 - Will
7
如果我告诉你我来这里是因为我正在进行优化,而不是过早地进行,而是在我真正需要的时候?你假设 OP 想要过早地进行优化,但你知道这个网站有点全球化...对吧?我不想无礼,但下次请不要做出这样的假设。 - Dalibor Filus
1
@DaliborFilus 我需要找到一个平衡点。使用静态方法会导致各种问题,因此应该尽量避免,特别是在你不知道自己在做什么时。其次,大多数“慢速”代码是由于(糟糕的)设计,而不是因为所选语言很慢。如果你的代码很慢,静态方法可能无法帮助它,除非它调用的方法 什么也不做。在大多数情况下,方法中的代码将使调用开销相形见绌。 - Aaron Digulla
21
被踩了。这并没有回答问题。问题是关于性能优势的,而不是询问设计原则的意见。 - Colm Bhandal
8
如果我训练一只鹦鹉说“过早优化是万恶之源”,我会得到许多和这只鹦鹉一样懂得有关性能的人的投票支持。 - rghome
显示剩余2条评论

45

静态调用无法被覆盖(因此始终是内联候选项),并且不需要任何空值检查。HotSpot针对实例方法进行了许多很酷的优化,这可能会抵消这些优势,但这些是可能的原因,为什么静态调用可能更快。

然而,这不应影响您的设计 - 以最可读、最自然的方式编写代码 - 只有在有正当理由时才考虑这种微观优化(而这几乎是从不出现的情况)。


这些可能是静态调用更快的原因。你能解释一下这些原因吗? - JavaTechnical
6
这段话的意思是:答案解释了两个原因——不需要覆盖(这意味着你不需要每次都计算实现方法 * 并且 * 你可以内联)和你不需要检查是否在空引用上调用方法。 - Jon Skeet
6
@JavaTechnical:我不理解。我刚刚给了你一些静态方法不需要计算/检查的东西,并提供了内联的机会。不做工作本身就是一种性能优势。还有什么需要理解的? 我的翻译如下:@JavaTechnical:我不明白。我刚刚给你的是静态方法中不需要计算或检查的内容以及内联的机会。不进行额外的工作可以提高性能。有什么需要进一步理解的吗? - Jon Skeet
1
@JavaTechnical:好吧,没有空值检查需要执行 - 但是如果JIT编译器可以删除该检查(这将是上下文特定的),我不会期望有太大的差异。像内存是否在缓存中这样的事情会更重要。 - Jon Skeet
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/33832/discussion-between-javatechnical-and-jon-skeet - JavaTechnical
显示剩余2条评论

20

七年之后......

对于Mike Nakis发现的结果,我没有太大的信心,因为它们没有解决与热点优化有关的一些常见问题。我使用JMH仪器测试了基准测试,并发现在我的机器上,实例方法的开销约为静态调用的0.75%。考虑到这种低延迟,除非在最敏感的操作中,它在应用程序设计中可能不是最大的关注点。我的JMH基准测试的概要结果如下:

java -jar target/benchmark.jar

# -- snip --

Benchmark                        Mode  Cnt          Score         Error  Units
MyBenchmark.testInstanceMethod  thrpt  200  414036562.933 ± 2198178.163  ops/s
MyBenchmark.testStaticMethod    thrpt  200  417194553.496 ± 1055872.594  ops/s

你可以在Github上查看代码;

https://github.com/nfisher/svsi

这个基准测试本身很简单,但旨在最小化死代码消除和常量折叠。可能有其他优化措施被我忽略了,这些结果可能会因JVM版本和操作系统而有所不同。

package ca.junctionbox.svsi;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

class InstanceSum {
    public int sum(final int a, final int b) {
        return a + b;
    }
}

class StaticSum {
    public static int sum(final int a, final int b) {
        return a + b;
    }
}

public class MyBenchmark {
    private static final InstanceSum impl = new InstanceSum();

    @State(Scope.Thread)
    public static class Input {
        public int a = 1;
        public int b = 2;
    }

    @Benchmark
    public void testStaticMethod(Input i, Blackhole blackhole) {
        int sum = StaticSum.sum(i.a, i.b);
        blackhole.consume(sum);
    }

    @Benchmark
    public void testInstanceMethod(Input i, Blackhole blackhole) {
        int sum = impl.sum(i.a, i.b);
        blackhole.consume(sum);
    }
}

1
这里纯粹是出于学术兴趣。我对这种微观优化可能对除了 ops/s 以外的指标(例如内存使用、减少 .oat 文件大小等)有什么潜在好处感到好奇。您知道有哪些相对简单的工具/方法可以尝试对这些其他指标进行基准测试吗? - Ryan Thomas
2
热点发现类路径中没有InstanceSum的扩展。尝试添加另一个继承InstanceSum并覆盖该方法的类。 - milan

20

它取决于编译器/虚拟机。

  • 从理论上说,静态调用可以更高效一些,因为它不需要进行虚函数查找,并且还可以避免隐藏的"this"参数的开销。
  • 在实践中,许多编译器都会优化掉这种差异。

因此,除非您已经确定将其视为应用程序中真正关键的性能问题,否则可能没有必要担心它。过早地优化是万恶之源等等......

然而,我确实看到过在以下情况下,这种优化可以显著提高性能:

  • 执行非常简单的数学计算的方法,没有内存访问
  • 在紧密的内部循环中数百万次调用的方法
  • CPU绑定应用程序,每一个性能方面都很重要

如果上述情况适用于您,则值得测试。

还有另一个很好(甚至可能更重要!)使用静态方法的原因-如果该方法实际上具有静态语义(即在逻辑上与类的给定实例无关),那么将其设为静态是有意义的,以反映这一事实。经验丰富的Java程序员会注意到静态修饰符并立即想到“啊哈!这个方法是静态的,所以它不需要实例,可能不会操纵特定实例的状态”。因此,您已经有效地传达了该方法的静态性质....


15

正如之前的帖子所说:这似乎是一种过早的优化。

然而,有一个区别(除了额外将被调用对象推送到操作数栈上的非静态调用):

由于静态方法无法被覆盖,静态方法调用在运行时不会进行任何虚拟查找。在某些情况下,这可能会导致可观察到的差异。

字节码级别上的差异是,非静态方法调用通过INVOKEVIRTUALINVOKEINTERFACEINVOKESPECIAL完成,而静态方法调用则通过INVOKESTATIC完成。


2
一个私有实例方法,通常使用invokespecial调用,因为它不是虚拟的。 - Mark Peters
啊,有趣,我只能想到构造函数,所以我省略了它!谢谢!(更新答案) - aioobe
2
如果只有一个类型被实例化,JVM 将进行优化。如果 B 扩展 A,并且没有实例化 B 的实例,则对 A 的方法调用将不需要虚拟表查找。 - Steve Kuo

12

在您的应用程序中,静态调用与非静态调用的表现有所不同是相当不可能的。请记住,“过早优化是万恶之源”。


这是Knuth!http://shreevatsa.wordpress.com/2008/05/16/premature-optimization-is-the-root-of-all-evil/ - ShreevatsaR
请问您能否进一步解释一下“过早优化是万恶之源”这句话的含义? - user2121
问题是“有没有任何一种方式有性能优势?”,这正好回答了这个问题。 - DJClayworth

12

在决定一个方法是否应该是静态的时候,性能方面应该是无关紧要的。如果你有性能问题,那么将许多方法改成静态并不会挽救局面。话虽如此,在大多数情况下,静态方法几乎肯定不比任何实例方法慢,通常稍微更快

1.) 静态方法不是多态的,因此JVM需要做出更少的决策来找到要执行的实际代码。在热点(HotSpot)时代,这是一个无关紧要的点,因为热点会优化只有一个实现位置的实例方法调用,所以它们的表现相同。

2.) 另一个微妙的区别是静态方法显然没有"this"引用。这导致栈帧比具有相同签名和主体的实例方法小一个插槽("this"放在字节码级别的本地变量中的插槽0中,而对于静态方法,插槽0用于方法的第一个参数)。


5

对于任何一个特定的代码片段,可能存在差异并且可能朝着任意一种方式发展,甚至在JVM的次要版本中也可能发生变化。

这绝对是应该忘记的97%小效率的一部分


2
错误。你不能做任何假设。这可能是一个前端UI所需的紧密循环,这可能会对UI的“敏捷性”产生巨大影响。例如,在TableView中搜索数百万条记录。 - trilogy

0

理论上来说,更便宜。

即使您创建对象的实例,静态初始化也将完成,而静态方法通常不会执行构造函数中通常执行的任何初始化。

但是,我还没有测试过这个。


1
@R. Bemrose,静态初始化与这个问题有什么关系? - Kirk Woll
@Kirk Woll:因为静态初始化是在第一次引用类时完成的... 包括在第一次静态方法调用之前。 - Powerlord
@R. Bemrose,当然,就像将类加载到VM中一样。在我看来,这似乎是一个不相关的论点。 - Kirk Woll

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