106

以一种或另一种方式有任何性能优势吗?它是编译器/虚拟机特定的吗?我正在使用热点。

4

12 回答 12

81

首先:您不应该根据性能来选择静态与非静态。

第二:在实践中,它不会有任何区别。Hotspot 可能会选择以使一种方法的静态调用更快、另一种方法的非静态调用更快的方式进行优化。

第三:围绕静态与非静态的许多神话要么基于非常古老的 JVM(它没有在 Hotspot 所做的优化附近做任何事情),要么基于一些记忆中的关于 C++ 的琐事(其中动态调用使用更多的内存访问比静态调用)。

于 2010-09-27T15:13:58.517 回答
74

四年后...

好的,为了一劳永逸地解决这个问题,我编写了一个基准来显示不同类型的调用(虚拟、非虚拟、静态)如何相互比较。

在 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 指令集。)如果您希望通过用非虚拟或静态调用替换它们来消除它们,最终不得不添加尽可能多的额外代码来模拟它们的功能,那么您产生的净开销是有限的不是更少,而是更多。很可能,很多,很多,深不可测的很多,更多。

于 2015-02-14T00:38:03.707 回答
45

好吧,静态调用不能被覆盖(因此始终是内联的候选者),并且不需要任何无效性检查。HotSpot 对实例方法进行了一系列很酷的优化,这些优化可能会抵消这些优势,但它们可能是静态调用可能更快的原因。

但是,这不应该影响您的设计 - 以最易读、最自然的方式编写代码 - 如果您有正当理由(您几乎永远不会这样做) ,只需担心这种微优化。

于 2010-09-27T15:14:28.203 回答
19

它是编译器/虚拟机特定的。

  • 理论上,静态调用可以稍微高效一些,因为它不需要进行虚函数查找,并且还可以避免隐藏的“this”参数的开销。
  • 实际上,无论如何,许多编译器都会对此进行优化。

因此,除非您已将其确定为应用程序中真正关键的性能问题,否则可能不值得为此烦恼。过早的优化是万恶之源……

但是,我已经看到这种优化在以下情况下显着提高了性能:

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

如果以上适用于您,则可能值得测试。

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

于 2010-09-27T15:39:28.143 回答
16

7年后...

我对 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);
    }
}
于 2017-12-26T21:05:39.620 回答
14

正如之前的海报所说:这似乎是一个过早的优化。

但是,有一个区别(部分原因是非静态调用需要将被调用对象额外推送到操作数堆栈上):

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

字节码级别的区别在于非静态方法调用是通过 完成的INVOKEVIRTUALINVOKEINTERFACE或者INVOKESPECIAL静态方法调用是通过 完成的INVOKESTATIC

于 2010-09-27T15:16:00.693 回答
12

令人难以置信的是,静态调用与非静态调用的任何性能差异都不会对您的应用程序产生影响。请记住“过早的优化是万恶之源”。

于 2010-09-27T15:13:02.293 回答
12

对于方法是否应该是静态的决定,性能方面应该是无关紧要的。如果您遇到性能问题,那么将许多方法设为静态并不能化险为夷。也就是说,静态方法几乎肯定不会比任何实例方法慢,在大多数情况下稍微快一点

1.) 静态方法不是多态的,因此 JVM 在查找要执行的实际代码时做出的决定较少。这是 Hotspot 时代的一个争论点,因为 Hotspot 将优化只有一个实现站点的实例方法调用,因此它们将执行相同的操作。

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

于 2010-09-27T16:02:23.577 回答
5

可能会有所不同,并且对于任何特定的代码段都可能会发生变化,甚至可能会随着 JVM 的次要版本而改变。

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

于 2010-09-27T15:17:22.677 回答
0

理论上,更便宜。

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

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

于 2010-09-27T15:13:50.527 回答
0

正如 Jon 所指出的,静态方法不能被覆盖,因此简单地调用静态方法可能——在足够幼稚的 Java 运行时——比调用实例方法更快。

但是,即使假设您正处于关心搞砸设计以节省几纳秒的地步,这也带来了另一个问题:您是否需要覆盖自己的方法?如果您更改代码以将实例方法变为静态方法以在这里和那里节省一纳秒,然后转身并在此基础上实现您自己的调度程序,那么您的调度程序几乎肯定会比构建的效率低已经进入您的 Java 运行时。

于 2010-09-27T15:39:21.733 回答
-2

我想在这里添加其他很好的答案,这也取决于您的流程,例如:

Public class MyDao {

   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, new MyRowMapper());
   };
};

请注意,每次调用都会创建一个新的 MyRowMapper 对象。
相反,我建议在这里使用一个静态字段。

Public class MyDao {

   private static RowMapper myRowMapper = new MyRowMapper();
   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, myRowMapper);
   };
};
于 2015-07-10T00:10:25.717 回答