Java静态调用比非静态调用昂贵还是便宜?


Answers:


74

首先:您不应基于性能来选择静态还是非静态。

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

第三:关于静态和非静态的许多神话都是基于非常老的JVM(在Hotspot所做的优化附近没有做任何事情),或者是一些关于C ++的记住的琐事(动态调用使用更多的内存访问权)比静态调用)。


1
完全正确,您不应该仅基于此一个方法而不喜欢静态方法。但是,在静态方法非常适合设计的情况下,知道它们至少和实例方法一样快(如果不比实例方法快),并且不应该从性能上排除它,这很有用。
威尔

2
@AaronDigulla -.-如果我告诉你我来这里是因为我现在而不是过早地进行优化,而是在我真正需要它时进行优化,该怎么办?您以为OP会过早地进行优化,但您知道此站点有点像全球站点...对吗?我不想变得无礼,但是下次请不要假设这种事情。
达利博尔·菲卢斯

1
@DaliborFilus我需要找到一个平衡点。使用静态方法会导致各种问题,因此应避免使用它们,尤其是当您不知道自己在做什么的时候。其次,大多数“慢”代码是由于(糟糕的)设计,而不是因为选择语言很慢。如果您的代码运行缓慢,则静态方法可能不会保存它,除非其调用方法绝对不起作用。在大多数情况下,代码的方法将大大超过了调用的开销。
亚伦·迪古拉

6
不赞成投票。这不能回答问题。该问题询问了性能收益。它没有征求有关设计原则的意见。
Colm Bhandal '18

4
如果我训练一只鹦鹉说“过早的优化是万恶之源”,我将从与鹦鹉一样了解性能的人那里获得1000票。
rghome

62

四年后...

好的,希望一劳永逸地解决这个问题,我编写了一个基准测试,该基准测试显示了不同类型的调用(虚拟,非虚拟,静态)之间如何进行比较。

在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的额外代码来模拟它们的功能,那么必然会产生净费用不少于,但更多。可能很多很多,很多很多。


7
“虚拟”是C ++术语。Java中没有虚拟方法。有普通方法(不是运行时多态的)和静态方法或final方法(不是)。
2015年

16
@levgen是的,对于一个像官方语言高级概述一样狭narrow的人,这正是您所说的。但是,当然,高级概念是使用完善的低级机制来实现的,这些机制是在Java出现之前很久才发明的,虚拟方法就是其中之一。如果您只看下面的内容,您会立即发现它是:docs.oracle.com/javase/specs/jvms/se7/html/…
麦克Nakis

13
感谢您在不作过早优化的前提下回答问题。好答案。
vegemite4me,2016年

3
是的,这正是我的意思。无论如何,我只是在机器上运行了测试。除了可以预期的基准测试抖动之外,速度没有任何区别:VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198在我的OpenJDK安装上。FTR:如果删除final修饰符,这甚至是正确的。顺便说一句。我必须参加terminate比赛 volatile,否则测试未完成。
Marten 2013年

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

46

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

但是,这不应该影响您的设计-以最易读,最自然的方式编写代码-仅在有正当理由的情况下才担心这种微优化( 永远不会)。


它们可能是导致静态呼叫速度更快的原因,您能解释一下这些原因吗?
JavaTechnical

6
@JavaTechnical:答案说明了这些原因-无需重载(这意味着您不必每次都制定出可以使用的实现,并且可以内联),也不需要检查是否在a上调用了该方法空引用。
乔恩·斯基特

6
@JavaTechnical:我不明白。我刚刚为您提供了不需要为静态方法进行计算/检查的内容,以及一个内联的机会。不工作带来性能上的好处。还剩下什么要理解?
乔恩·斯基特

静态变量的检索速度比非静态变量快吗?
JavaTechnical

1
@JavaTechnical:嗯,没有要执行的空值检查-但是,如果JIT编译器可以删除该检查(这将是特定于上下文的),则不会有太大的区别。诸如内存是否在高速缓存中之类的事情将更为重要。
乔恩·斯基特

18

它特定于编译器/ VM。

  • 从理论上讲,可以使静态调用的效率稍高一些,因为它不需要执行虚函数查找,而且还可以避免隐藏的“ this”参数的开销。
  • 实际上,许多编译器仍然会对此进行优化。

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

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

  • 无需存储器访问即可执行非常简单的数学计算的方法
  • 方法被数百万调用在紧密的内部循环中每秒次
  • CPU约束的应用程序,每一个性能都很重要

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

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


14

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

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

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

在字节代码级的区别在于非静态方法调用是通过完成INVOKEVIRTUALINVOKEINTERFACEINVOKESPECIAL同时静态方法调用是通过完成INVOKESTATIC


2
但是,(至少通常)使用私有实例方法来调用,invokespecial因为它不是虚拟的。
马克·彼得斯2010年

嗯,有趣的是,我只能想到构造函数,这就是为什么我省略了它!谢谢!(更新答案)
aioobe

2
如果实例化了一种类型,JVM将进行优化。如果B扩展了A,并且没有实例化B的实例,则对A的方法调用将不需要虚拟表查找。
郭富城2010年


13

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

您可以在Github上查看代码;

https://github.com/nfisher/svsi

基准测试本身非常简单,但是旨在最大程度地减少死代码消除和持续折叠。我可能遗漏了一些其他优化/忽略了这些优化,并且这些结果可能因JVM版本和OS而异。

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主要在ART环境以外的其他指标可能产生的潜在好处(例如,内存使用量,减少的.oat文件大小等)。您是否知道有任何相对简单的工具/方法可以尝试对这些其他指标进行基准测试?
瑞安·托马斯

热点表明,类路径中没有InstanceSum的扩展。尝试添加另一个扩展InstanceSum并覆盖该方法的类。
米兰

12

为了确定方法是否应该是静态的,性能方面应该无关紧要。如果您遇到性能问题,那么将许多方法设为静态将无济于事。也就是说,静态方法几乎肯定不会比任何实例方法都要,在大多数情况下要快一些

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

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


5

可能有所不同,对于任何特定的代码段来说,它可能都是相反的,即使是较小的JVM版本,它也可能会发生变化。

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


2
错误。你不能承担任何责任。前端UI可能需要紧密循环,这可能会极大地改变UI的“灵活度”。例如,搜索TableView成千上万的记录。
三部曲

0

从理论上讲,价格便宜。

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

但是,我尚未对此进行测试。


1
@R。Bemrose,静态初始化与这个问题有什么关系?
Kirk Woll 2010年

@Kirk Woll:因为静态初始化是在第一次引用该类时完成的,所以……包括在第一次静态方法调用之前。
Powerlord

@R。当然,Bemrose就像将类加载到VM中一样。好像是非序列人,IMO。
Kirk Woll 2010年

0

正如乔恩(Jon)所言,静态方法不能被覆盖,因此,在足够幼稚的Java运行时上,简单地调用静态方法可能比调用实例方法要快。

但是,即使假设您正处在考虑将设计弄乱以节省几纳秒的时候,这也提出了另一个问题:您是否需要方法来超越自己?如果您将代码更改为将实例方法转换为静态方法以在其中四处节省十亿分之一秒,然后又在其周围实现自己的调度程序,则几乎可以肯定,您的调度程序的效率将低于构建的调度程序。到您的Java运行时中。


-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);
   };
};
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.