Java静态调用是否比非静态调用更昂贵或更便宜?

And*_*nko 81 java performance premature-optimization

这种或那种方式有任何性能上的好处吗?它是编译器/ VM特定的吗?我正在使用Hotspot.

Ano*_*non 71

第一:你不应该在性能的基础上选择静态与非静态.

第二:在实践中,它不会有任何区别.热点可以选择以一种方式优化以使一种方法更快地进行静态调用,而对另一种方法更快地进行非静态调用.

第三:围绕静态与非静态的大多数神话都基于非常古老的JVM(在Hotspot所做的优化附近没有做任何事情),或者是一些关于C++的记忆琐事(其中动态调用使用了一个以上的内存访问)而不是静态电话).

  • 如果我训练一只鹦鹉说“过早优化是万恶之源”,我会从那些和鹦鹉一样了解性能的人那里获得 1000 票。 (6认同)
  • Downvoted.这不回答这个问题.问题是关于性能的好处.它没有就设计原则征求意见. (5认同)
  • @AaronDigulla -.-如果我告诉你我来这里是因为我现在而不是过早地进行优化,而是在我真正需要它时进行优化,该怎么办?您以为OP会过早地进行优化,但您知道此站点有点像全球站点...对吗?我不想变得无礼,但是下次请不要假设这种事情。 (2认同)

Mik*_*kis 53

四年后......

好的,为了一次又一次地解决这个问题,我编写了一个基准测试,显示了不同类型的调用(虚拟,非虚拟,静态)如何相互比较.

在ideone上运行它,这就是我得到的:

(更大的迭代次数更好.)

    Success time: 3.12 memory: 320576 signal:0
  Name          |  Iterations
    VirtualTest |  128009996
 NonVirtualTest |  301765679
     StaticTest |  352298601
Done.
Run Code Online (Sandbox Code Playgroud)

正如所料,虚方法调用是最慢的,非虚方法调用更快,静态方法调用甚至更快.

我没想到的是差异是如此明显:虚拟方法调用的运行速度不到非虚拟方法调用的一半,而非虚拟方法调用的速度比静态调用慢15%.这就是这些测量显示的结果; 事实上,实际差异必须稍微明显一些,因为对于每个虚拟,非虚拟和静态方法调用,我的基准测试代码都有一个额外的常量开销,即递增一个整数变量,检查一个布尔变量,如果不是,则循环.

我想结果会因CPU和CPU以及JVM到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) {}
    } );
}
Run Code Online (Sandbox Code Playgroud)

值得注意的是,这种性能差异仅适用于除了调用无参数方法之外什么都不做的代码.无论调用之间的其他代码是什么,都会稀释差异,这包括参数传递.实际上,静态和非虚拟调用之间15%的差异可能完全this指针不必传递给静态方法的事实来解释.因此,只需要相当少量的代码就可以在不同类型的调用之间的差异调用之间做一些微不足道的事情,将其稀释到没有任何净影响的程度.

此外,存在虚拟方法调用的原因; 它们确实有服务的目的,它们是使用底层硬件提供的最有效的方法实现的.(CPU指令集.)如果您希望通过用非虚拟或静态调用替换它们来消除它们,那么您最终必须添加尽可能多的额外代码来模拟它们的功能,然后您的结果净开销受到限制不少,但更多.很可能,非常多,不可思议地多,更多.

  • @levgen是的,对于那些观点与语言的官方高级概述一样狭窄的人来说,正如你所说的那样.但是当然,高级概念是使用已经建立的低级机制来实现的,这些机制是在Java出现之前很长一段时间发明的,而虚拟方法就是其中之一.如果你只是在引擎盖下看一下,你会立即看到它是这样的:http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5. invokevirtual (13认同)
  • 感谢您在不对过早优化做出假设的情况下回答问题.很好的答案. (10认同)
  • 'Virtual'是一个C++术语.Java中没有虚拟方法.有一些普通的方法,它们是运行时多态的,而静态或最终的方法则不是. (7认同)
  • 是的,这正是我的意思.无论如何,我只是在我的机器上运行测试.除了你可以期待的这种基准测试的抖动之外,速度没有任何区别:`VirtualTest | 488846733 - NonVirtualTest | 480530022 - StaticTest | 我的OpenJDK安装上的484353198`.FTR:如果我删除`final`修饰符,那就是真的.顺便说一句.我必须使`terminate`字段`volatile`,否则测试没有完成. (3认同)
  • 仅供参考,我在运行Android 6的Nexus 5上获得了相当惊人的结果:`VirtualTest | 12451872 - NonVirtualTest | 12089542 - StaticTest | 8181170`.不仅我的笔记本上的OpenJDK能够执行40倍以上的迭代,静态测试的吞吐量总是减少约30%.这可能是一种特定于ART的现象,因为我在Android 4.4平板电脑上得到了预期的结果:`VirtualTest | 138183740 - NonVirtualTest | 142268636 - StaticTest | 161388933` (3认同)
  • 感谢您显示基准数字!我发现这里的结果很有趣,特别是因为Intellij具有代码检查选项,该选项检查是否可以在不更改代码的情况下将方法声明为静态。 (2认同)
  • 在某些环境中,这个测试可以在不声明“terminate”为“易失性”的情况下完成,因为它运行冷,即解释,然后,这使得整个“基准”没有实际意义。实际上,任何性能关键代码位置都将得到优化,这不仅消除了方法调用开销,而且还消除了非易失性变量的冗余读取。在我的系统上,即使没有预热,它也会挂起。当然,向标志添加“易失性”修饰符也会使整个测试变得毫无价值。制定有意义的微基准测试并非易事。 (2认同)

Jon*_*eet 44

好吧,静态调用不能被覆盖(所以总是候选内联),并且不需要任何无效检查.HotSpot为实例方法做了很多很好的优化,这可能会抵消这些优势,但它们可能是静态调用可能更快的原因.

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

  • @JavaTechnical:答案解释了这些原因 - 没有覆盖(这意味着你不需要计算每次使用的实现*和*你可以内联)而且你不需要检查你是否正在调用方法在空引用上. (6认同)
  • @JavaTechnical:我不明白.我刚给你一些不需要计算/检查静态方法的东西,以及一个内联机会.不做工作*是一种性能优势.还有什么可以理解的? (6认同)

mik*_*era 18

它是特定于编译器/ VM的.

  • 理论上,静态调用可以稍微提高效率,因为它不需要进行虚函数查找,也可以避免隐藏的"this"参数的开销.
  • 实际上,许多编译器无论如何都会优化它.

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

不过,我已经看到了这个优化得到以下情况下大幅提高性能:

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

如果上述内容适用于您,则可能值得测试.

使用静态方法还有一个好的(可能更重要的!)理由 - 如果方法实际上具有静态语义(即逻辑上没有连接到类的给定实例)那么将它设置为静态是有意义的反映这一事实.经验丰富的Java程序员会注意到静态修饰符并立即想到"啊哈!这个方法是静态的,因此它不需要实例,并且可能不会操纵特定于实例的状态".所以你将有效地传达方法的静态性质....


aio*_*obe 14

正如之前的海报所说:这似乎是一个不成熟的优化.

但是,存在一个区别(非静态调用需要额外将被调用对象推送到操作数堆栈的事实):

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

字节码级别的差异在于非静态方法调用是通过完成INVOKEVIRTUAL,INVOKEINTERFACE或者INVOKESPECIAL在静态方法调用完成时完成的INVOKESTATIC.

  • 但是,私有实例方法(至少通常)使用`invokespecial`调用,因为它不是虚拟的. (2认同)
  • 如果实例化了一种类型,JVM将进行优化.如果B扩展A,并且没有实例化B的实例,则对A的方法调用将不需要虚拟表查找. (2认同)

DJC*_*rth 13

令人难以置信的是,静态调用与非静态调用的性能差异对您的应用程序产生了影响.请记住,"过早优化是所有邪恶的根源".


Dur*_*dal 12

对于决定方法是否应该是静态的,性能方面应该是无关紧要的.如果您遇到性能问题,那么将大量方法设置为静态就不会节省时间.也就是说,静态方法几乎肯定不比任何实例方法,在大多数情况下略快一些:

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

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


Nat*_*han 11

7年后......

我对Mike Nakis发现的结果没有很大的信心,因为他们没有解决与Hotspot优化相关的一些常见问题.我使用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
Run Code Online (Sandbox Code Playgroud)

您可以在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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • Hotspot 发现类路径中没有 InstanceSum 的扩展。尝试添加另一个扩展 InstanceSum 的类并重写该方法。 (2认同)

Mic*_*rdt 5

可能有所不同,对于任何特定的代码段来说,它可能会以任何一种方式出现,并且即使在较小的JVM版本中也可能会有所不同。

这绝对是您应该忽略的97%的小效率中的一部分。

  • 错误。你不能承担任何责任。前端UI可能需要紧密循环,这可能会极大地改变UI的“灵活度”。例如,在“ TableView”中搜索数百万条记录。 (2认同)