web-dev-qa-db-ja.com

Java静的呼び出しは非静的呼び出しよりも多かれ少なかれ費用がかかりますか?

何らかの方法でパフォーマンス上の利点はありますか?コンパイラ/ VM固有ですか?ホットスポットを使用しています。

77

まず、パフォーマンスに基づいて静的と非静的を選択するべきではありません。

2番目:実際には、違いはありません。ホットスポットは、あるメソッドでは静的呼び出しを高速化し、別のメソッドでは非静的呼び出しを高速化する方法で最適化することを選択できます。

3番目:静的と非静的を取り巻く神話の多くは、非常に古いJVM(Hotspotが行う最適化の近くでは何もしなかった)、またはC++に関するいくつかの記憶されたトリビア(動的呼び出しではone静的呼び出しよりも多くのメモリアクセス)。

70
Anon

4年後...

さて、この質問を永遠に解決することを期待して、さまざまな種類の呼び出し(仮想、非仮想、静的)が互いにどのように比較されるかを示すベンチマークを作成しました。

私はそれを実行しました ideone 、これは私が得たものです:

(反復回数が多いほど良い。)

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

予想どおり、仮想メソッド呼び出しは最も遅く、非仮想メソッド呼び出しはより高速で、静的メソッド呼び出しはさらに高速です。

私が予想していなかったのは、その違いが非常に顕著であるということです。仮想メソッド呼び出しは、非仮想メソッド呼び出しの速度であるhalfで実行されるように測定されました。静的呼び出しよりも全体で15%遅いを実行するように測定されました。それがこれらの測定値が示すものです。実際には、仮想、非仮想、および静的メソッドの呼び出しごとに、ベンチマークコードには1つの整数変数をインクリメントし、ブール変数をチェックし、真でない場合はループするという追加の一定のオーバーヘッドがあるため、実際の違いはわずかに顕著でなければなりません。

結果は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ポインターを静的メソッドに渡す必要がないという事実によってin fullで説明されています。したがって、呼び出しの間にささいなことをするかなり少量のコードだけで、異なる種類の呼び出しの違いを、最終的な影響がまったくない点まで希釈することができます。

また、仮想メソッド呼び出しには理由があります。それらには役立つ目的があり、基盤となるハードウェアによって提供される最も効率的な手段を使用して実装されます。 (CPU命令セット。)それらを非仮想呼び出しまたは静的呼び出しに置き換えることによってそれらを排除したい場合、機能をエミュレートするために余分なコードのiotaほどを追加する必要があり、最終的なネットオーバーヘッドが制限されます。より少なくないが、より多くに。おそらく、はるかに、はるかに、計り知れないほど、もっと。

51
Mike Nakis

さて、静的な呼び出しすることはできませんオーバーライドされ(常にインライン化の候補となります)、nullityチェックを必要としません。 HotSpotは、これらの利点を十分に無効にする可能性のあるインスタンスメソッドに対して多数のクールな最適化を行いますが、それらはpossible静的呼び出しが高速になる理由です。

しかし、それはあなたのデザインに影響を与えるべきではありません-最も読みやすく、自然な方法でコード-そしてあなたがちょうど原因がある場合にのみ、この種のマイクロ最適化について心配する必要があります(ほとんどnever意志)。

43
Jon Skeet

コンパイラ/ VM固有です。

  • 理論上、仮想関数のルックアップを行う必要がないため、静的呼び出しをわずかに効率化できます。また、隠された「this」パラメーターのオーバーヘッドを回避できます。
  • 実際には、多くのコンパイラはとにかくこれを最適化します。

したがって、アプリケーションでこれが本当に重大なパフォーマンスの問題であると特定しない限り、おそらく気にする価値はありません。時期尚早の最適化は、すべての悪などの根源です...

しかし、私はhaveがこの最適化により次の状況でパフォーマンスが大幅に向上することを見ました:

  • メモリアクセスなしで非常に簡単な数学的計算を実行する方法
  • 呼び出されているメソッド数百万タイトな内部ループで1秒あたりの時間
  • あらゆるパフォーマンスが重要なCPUバウンドアプリケーション

上記が当てはまる場合は、テストする価値があるかもしれません。

また、静的メソッドを使用するもう1つの良い(そして潜在的にさらに重要な!)理由があります-メソッドが実際に静的セマンティクスを持っている場合(つまり、論理的にクラスの特定のインスタンスに接続されていない場合)、それを静的にすることは理にかなっていますこの事実を反映するために。経験豊富なJavaプログラマーは静的修飾子に気付き、すぐに「このメソッドは静的であるため、インスタンスを必要とせず、おそらくインスタンス固有の状態を操作しない」と考えます。メソッドの静的な性質を効果的に伝えています。

18
mikera

前のポスターが言ったように:これは時期尚早な最適化のようです。

ただし、1つの違いがあります(非静的呼び出しには、呼び出し先オブジェクトをオペランドスタックに追加プッシュする必要があるという事実からの一部)。

静的メソッドはオーバーライドできないため、静的メソッド呼び出しの実行時に仮想ルックアップはありません。これにより、状況によっては目に見える違いが生じる場合があります。

バイトコードレベルの違いは、非静的メソッド呼び出しが INVOKEVIRTUALINVOKEINTERFACE または INVOKESPECIAL 静的メソッド呼び出しは INVOKESTATIC を介して行われます。

14
aioobe

静的呼び出しと非静的呼び出しのパフォーマンスの違いがアプリケーションに影響を与えることは、信じられないほどありそうにありません。 「時期尚早の最適化がすべての悪の根源である」ことを忘れないでください。

12
DJClayworth

メソッドを静的にする必要があるかどうかを判断するには、パフォーマンスの側面は無関係である必要があります。パフォーマンスに問題がある場合、多くのメソッドを静的にすることで時間を節約できません。とはいえ、静的メソッドはほぼ確実に 遅くない ほとんどの場合、どのインスタンスメソッドよりも わずかに速い

1.)静的メソッドはポリモーフィックではないため、実行する実際のコードを見つけるためにJVMが下す決定はほとんどありません。これは、ホットスポットが実装サイトを1つしか持たないインスタンスメソッド呼び出しを最適化し、同じことを実行するため、Age of Hotspotの重要なポイントです。

2.)別の微妙な違いは、静的メソッドには明らかに「this」参照がないことです。これにより、スタックフレームは、同じシグネチャとボディを持つインスタンスメソッドのスタックフレームよりも1スロット小さくなります(「this」は、バイトコードレベルでローカル変数のスロット0に配置されますが、静的メソッドでは、最初のメソッドのパラメーター)。

11
Durandal

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

違いがあるかもしれませんし、コードの特定の部分についてはどちらの方向にも行くかもしれませんし、JVMのマイナーリリースでさえ変わるかもしれません。

これは最も確実に 忘れてはならない小さな効率の97% の一部です。

4

理論的には、安価です。

オブジェクトのインスタンスを作成しても静的初期化は行われますが、静的メソッドはコンストラクターで通常行われる初期化を行いません。

ただし、これはテストしていません。

0
Powerlord

Jonが指摘しているように、静的メソッドはオーバーライドできないため、単純に呼び出し静的メソッドは-十分に素朴なJavaランタイム-より速い- 呼び出しインスタンスメソッド。

しかし、数ナノ秒を節約するために設計を台無しにしたいと思っている場合でも、別の質問が出てきます:自分でオーバーライドする方法が必要ですか?コードを変更してインスタンスメソッドを静的メソッドに変更し、あちこちでナノ秒を節約し、さらにその上に独自のディスパッチャーを実装すると、ビルドされたものよりも確実に効率が低下しますJavaランタイムに既に。

0
Ken