【问题标题】:Are Java static calls more or less expensive than non-static calls?Java 静态调用比非静态调用更贵还是更便宜?
【发布时间】:2011-04-17 20:04:58
【问题描述】:

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

【问题讨论】:

    标签: java performance premature-optimization


    【解决方案1】:

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

    【讨论】:

    • 这里纯粹是学术兴趣。我很好奇这种微优化可能对ops/s 以外的指标产生的任何潜在好处,主要是在 ART 环境中(例如内存使用、减少 .oat 文件大小等)。您是否知道可以尝试对这些其他指标进行基准测试的任何相对简单的工具/方法?
    • Hotspot 发现类路径中没有对 InstanceSum 的扩展。尝试添加另一个扩展 InstanceSum 并覆盖该方法的类。
    【解决方案2】:

    四年后...

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

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

    【讨论】:

    • “虚拟”是一个 C++ 术语。 Java 中没有虚拟方法。有普通方法,它们是运行时多态的,也有静态或最终方法,它们不是。
    • @levgen 是的,对于那些观点与该语言的官方高级概述一样狭隘的人来说,正如你所说。但是,当然,高级概念是使用在 Java 出现之前很久就发明的完善的低级机制来实现的,虚拟方法就是其中之一。如果您稍微看一下引擎盖下的内容,您会立即发现是这样的:docs.oracle.com/javase/specs/jvms/se7/html/…
    • 感谢您在没有对过早优化做出假设的情况下回答问题。很好的答案。
    • 是的,这正是我的意思。无论如何,我只是在我的机器上运行了测试。除了您对此类基准测试的预期抖动之外,速度没有任何差异:我的 OpenJDK 安装上的VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198。 FTR:即使我删除了final 修饰符,情况也是如此。顺便提一句。我必须将terminate 字段设为volatile,否则测试没有完成。
    • 仅供参考,我在运行 Android 6 的 Nexus 5 上得到了相当令人惊讶的结果:VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170。不仅我笔记本上的 OpenJDK 能够执行 40 倍以上的迭代,静态测试的吞吐量也总是降低 30% 左右。这可能是 ART 特有的现象,因为我在 Android 4.4 平板电脑上得到了预期的结果:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

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

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

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

      字节码级别的区别在于,非静态方法调用通过INVOKEVIRTUALINVOKEINTERFACEINVOKESPECIAL完成,而静态方法调用通过INVOKESTATIC完成。

      【讨论】:

      • 然而,私有实例方法(至少通常)使用invokespecial 调用,因为它不是虚拟的。
      • 啊,有趣,我只能想到构造函数,所以我省略了它!谢谢! (更新答案)
      • 如果实例化了一种类型,JVM 将进行优化。如果 B 扩展了 A,并且没有实例化 B 的实例,则对 A 的方法调用将不需要虚拟表查找。
      【解决方案5】:

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

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

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

      【讨论】:

        【解决方案6】:

        它是编译器/VM 特定的。

        • 理论上,可以进行静态调用 稍微更有效率,因为它 不需要做虚函数 查找,它还可以避免 隐藏的“这个”的开销 参数。
        • 在实践中,许多编译器会 无论如何都要优化它。

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

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

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

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

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

        【讨论】:

          【解决方案7】:

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

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

          【讨论】:

            【解决方案8】:

            可能存在差异,对于任何特定的代码段都可能存在差异,甚至可能会随着 JVM 的次要版本而改变。

            这绝对是the 97% of small efficiencies that you should forget about 的一部分。

            【讨论】:

            • 错了。你不能假设任何事情。这可能是前端 UI 所需的紧密循环,这可能会对 UI 的“活泼”程度产生巨大影响。例如,在 TableView 中搜索数百万条记录。
            【解决方案9】:

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

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

            【讨论】:

            • 它们是静态调用可能更快的可能原因你能解释一下这些原因吗?
            • @JavaTechnical:答案解释了这些原因 - 没有覆盖(这意味着您不需要制定每次使用的实现并且您可以内联)并且您不需要检查您是否在空引用上调用该方法。
            • @JavaTechnical:我不明白。我刚刚为您提供了不需要为静态方法计算/检查的东西,以及内联机会。不工作一种性能优势。还有什么要理解的?
            • 静态变量的检索速度比非静态变量快吗?
            • @JavaTechnical:好吧,没有要执行的空值检查 - 但如果 JIT 编译器可以删除该检查(这将是特定于上下文的),我不希望有太大的区别。诸如内存是否在缓存中之类的事情会更重要。
            【解决方案10】:

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

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

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

            【讨论】:

            • 你说得对,你不应该仅仅基于这个而更喜欢静态方法。但是,如果静态方法非常适合设计,那么知道它们至少与实例方法一样快(如果不快的话)是有用的,并且不应该在性能基础上排除。
            • @AaronDigulla -.- 如果我告诉你我来这里是因为我现在正在优化,而不是过早地优化,而是当我真正需要它的时候呢?您假设 OP 想要过早地进行优化,但您知道这个站点有点全球化……对吧?我不想无礼,但请不要假设下次这样的事情。
            • @DaliborFilus 我需要找到一个平衡点。使用静态方法会导致各种问题,因此应避免使用它们,尤其是当您不知道自己在做什么时。其次,大多数“慢”代码是因为(糟糕的)设计,而不是因为 Language-of-Choice 很慢。如果你的代码很慢,静态方法可能不会保存它,除非它的调用方法什么都不做。在大多数情况下,in 方法中的代码将使调用开销相形见绌。
            • 投反对票。这没有回答问题。该问题涉及性能优势。它没有就设计原则征求意见。
            • 如果我训练一只鹦鹉说“过早的优化是万恶之源”,我会得到 1000 票来自与鹦鹉一样了解性能的人。
            【解决方案11】:

            理论上,更便宜。

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

            但是,我没有对此进行测试。

            【讨论】:

            • @R. Bemrose,静态初始化和这个问题有什么关系?
            • @Kirk Woll:因为静态初始化是在第一次引用类时完成的......包括在第一次静态方法调用之前。
            • @R.当然,Bemrose 就像一开始就将类加载到 VM 中一样。看起来像一个非追随者,IMO。
            【解决方案12】:

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

            【讨论】:

            • 能否进一步解释一下“过早的优化是万恶之源”是什么意思?
            • 问题是“是否有任何性能优势?”,这正是回答了这个问题。
            猜你喜欢
            • 2015-03-27
            • 1970-01-01
            • 2017-02-04
            • 1970-01-01
            • 2013-03-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多