同等の静的メソッドと非静的メソッドの速度の大きな違い


86

このコードでは、mainメソッドでオブジェクトを作成し、そのオブジェクトメソッドを呼び出すとff.twentyDivCount(i)(16010ミリ秒で実行)、このアノテーションを使用して呼び出すよりもはるかに高速にtwentyDivCount(i)実行されます:( 59516ミリ秒で実行)。もちろん、オブジェクトを作成せずに実行する場合は、メソッドを静的にするので、メインで呼び出すことができます。

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {    // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way
                       // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

編集:これまでのところ、マシンが異なれば結果も異なるようですが、JRE 1.8。*を使用すると、元の結果が一貫して再現されるように見えます。


4
ベンチマークをどのように実行していますか?これは、JVMがコードを最適化するのに十分な時間がないことによるアーティファクトだと思います。
パトリックコリンズ

2
+PrintCompilation +PrintInlining示されているように、JVMがメインメソッドのOSRをコンパイルして実行するのに十分な時間のようです
Tagir Valeev 2015年

1
コードスニペットを試しましたが、Stabbzが言ったような時差はありません。それらは56282ms(インスタンスを使用)54551ms(静的メソッドとして)。
ドンチャッカパン2015年

1
@ PatrickCollins5秒で十分です。私はそれを少し書き直しあなたは両方を測定できるように、(1 JVMがバリアントごとに開始されます)。ベンチマークとしてはまだ欠陥があることは知っていますが、十分に説得力があります。1457ミリ秒のSTATICと5312ミリ秒のNON_STATICです。
maaartinus

1
質問の詳細はまだ調査していませんが、これ関連している可能性あります:shipilev.net/blog/2015/black-magic-method-dispatch(多分AlekseyShipilëvはここで私たちを啓発することができます)
Marco13 2015年

回答:


72

JRE 1.8.0_45を使用すると、同様の結果が得られます。

調査:

  1. -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningVMオプションを指定してJavaを実行すると、両方のメソッドがコンパイルされ、インライン化されることが示されます。
  2. メソッド自体について生成されたアセンブリを見ると、大きな違いはありません。
  3. ただし、インライン化されると、生成されたアセンブリは mainでは大きく異なり、インスタンスメソッドは、特にループ展開に関して、より積極的に最適化されます。

次に、上記の疑いを確認するために、ループ展開設定を変えてテストを再度実行しました。私はあなたのコードを次のように実行しました:

  • -XX:LoopUnrollLimit=0 どちらのメソッドも実行速度が遅くなります(デフォルトオプションの静的メソッドと同様)。
  • -XX:LoopUnrollLimit=100 どちらのメソッドも高速に実行されます(デフォルトオプションのインスタンスメソッドと同様)。

結論として、デフォルト設定では、ホットスポット1.8.0_45のJIT、メソッドが静的な場合、ループを展開できないようです(ただし、なぜそのように動作するのかはわかりません)。他のJVMは異なる結果をもたらす可能性があります。


52から71の間で、元の動作が復元されます(少なくとも私のマシンでは、私の答えです)。静的バージョンは20ユニット大きかったようですが、なぜですか?これはおかしい。
maaartinus 2015年

3
@maaartinusその数が正確に何を表しているのかさえわかりません-ドキュメントはかなり回避的です:「サーバーコンパイラの中間表現ノード数がこの値より少ないループ本体を展開します。サーバーコンパイラが使用する制限はこの値の関数です。実際の値ではありません。デフォルト値は、JVMが実行されているプラ​​ットフォームによって異なります。 "...
assylias 2015年

どちらもわかりませんが、最初の推測では、静的メソッドはどのユニットでもわずかに大きくなり、重要な場所に到達しました。ただし、違いはかなり大きいので、私の現在の推測では、静的バージョンはかなり大きくなるいくつかの最適化を取得しています。生成されたasmは見ていません。
maaartinus 2015年

33

アッシリアの答えに基づいた、証明されていない推測。

JVMは、ループ展開のしきい値を使用します。これは70のようなものです。何らかの理由で、静的呼び出しはわずかに大きく、展開されません。

結果を更新する

  • LoopUnrollLimit52以下で、両方のバージョンが遅いです。
  • 52から71の間では、静的バージョンのみが低速です。
  • 71を超えると、どちらのバージョンも高速です。

私の推測では、静的呼び出しは内部表現でわずかに大きく、OPは奇妙なケースにぶつかったので、これは奇妙です。しかし、違いは約20のようで、意味がありません。

 

-XX:LoopUnrollLimit=51
5400 ms NON_STATIC
5310 ms STATIC
-XX:LoopUnrollLimit=52
1456 ms NON_STATIC
5305 ms STATIC
-XX:LoopUnrollLimit=71
1459 ms NON_STATIC
5309 ms STATIC
-XX:LoopUnrollLimit=72
1457 ms NON_STATIC
1488 ms STATIC

実験したい人にとっては、私のバージョンが役に立つかもしれません。


「1456ミリ秒」の時間ですか?もしそうなら、なぜ静的が遅いと言うのですか?
トニー

@Tony私は混乱NON_STATICしましたSTATICが、私の結論は正しかったです。今すぐ修正しました、ありがとうございます。
maaartinus 2015年

0

これをデバッグモードで実行すると、インスタンスと静的の場合の数値は同じになります。つまり、JITは、インスタンスメソッドの場合と同じように、静的な場合にコードをネイティブコードにコンパイルすることを躊躇します。

なぜそうするのですか?言うのは難しいです。これがより大きなアプリケーションであれば、おそらくそれは正しいことをするでしょう...


「なぜそうするのですか?言うのは難しいですが、これがより大きなアプリであれば、おそらく正しいことをするでしょう。」または、実際にデバッグするには大きすぎる奇妙なパフォーマンスの問題が発生する可能性があります。(そして言うのはそれほど難しいことではありません。JVMがassyliasのように吐き出すアセンブリを見ることができます。)
tmyklebu 2015年

@tmyklebuまたは、完全にデバッグするのに不必要で費用がかかる奇妙なパフォーマンスの問題があり、簡単な回避策があります。最後に、ここでJITについて話しますが、その作成者は、すべての状況でJITが正確にどのように動作するかを知りません。:)他の答えを見てください、それらは問題を説明するのに非常に良くそして非常に近いです、しかしそれでも今のところ誰もこれが正確に起こっている理由を知りません。
ドラガンボザノビッチ2015年

@DraganBozanovic:実際のコードで実際の問題が発生すると、「完全にデバッグする必要がない」ということはなくなります。
tmyklebu 2015年

0

テストを少し調整したところ、次の結果が得られました。

出力:

Dynamic Test:
465585120
232792560
232792560
51350 ms
Static Test:
465585120
232792560
232792560
52062 ms

注意

それらを別々にテストしている間、動的で最大52秒、静的で最大200秒かかりました。

これはプログラムです:

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {  // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    static int twentyDivCount2(int a) {
         int count = 0;
         for (int i = 1; i<21; i++) {

             if (a % i == 0) {
                 count++;
             }
         }
         return count;
    }

    public static void main(String[] args) {
        System.out.println("Dynamic Test: " );
        dynamicTest();
        System.out.println("Static Test: " );
        staticTest();
    }

    private static void staticTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        for (int i = start; i > 0; i--) {

            int temp = twentyDivCount2(i);

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }

    private static void dynamicTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

また、テストの順序を次のように変更しました。

public static void main(String[] args) {
    System.out.println("Static Test: " );
    staticTest();
    System.out.println("Dynamic Test: " );
    dynamicTest();
}

そして私はこれを手に入れました:

Static Test:
465585120
232792560
232792560
188945 ms
Dynamic Test:
465585120
232792560
232792560
50106 ms

ご覧のとおり、静的の前に動的が呼び出されると、静的の速度が劇的に低下しました。

このベンチマークに基づく:

私は仮説立てますはそれがすべてJVMの最適化に依存しているします。したがって、静的メソッドと動的メソッドの使用については、経験則に従うことをお勧めします。

経験則:

Java:静的メソッドをいつ使用するか


「静的メソッドと動的メソッドの使用については、経験則に従ってください。」この経験則は何ですか?そして、あなたは誰から/何から引用していますか?
ウェストン2015年

@weston申し訳ありませんが、私が考えていたリンクを追加しませんでした:)。thx
nafas 2015年

0

してみてください:

public class ProblemFive {
    public static ProblemFive PROBLEM_FIVE = new ProblemFive();

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();
        int start = 500000000;
        int result = start;


        for (int i = start; i > 0; i--) {
            int temp = PROBLEM_FIVE.twentyDivCount(i); // faster way
            // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
                System.out.println((System.currentTimeMillis() - startT) + " ms");
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();
        System.out.println((end - startT) + " ms");
    }

    int twentyDivCount(int a) {  // change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i < 21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }
}

20273ミリ秒から23000+ミリ秒、実行ごとに異なります
Stabbz 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.