9

次のコード スニペットのは、メソッドが呼び出されるFoo1たびにカウンターをインクリメントするクラスです。同じことを行いますが、間接化のレベルが 1 つ追加されます。bar()Foo2

Foo1よりも高速であると期待していますがFoo2、実際にFoo2は、常に よりも 40% 高速ですFoo1Foo2よりも高速に実行されるように、JVM はどのようにコードを最適化していますFoo1か?

いくつかの詳細

  • テストは で実行されましたjava -server CompositionTest
  • でテストを実行するとjava -client CompositionTest、予想される結果が得られますが、Foo2よりも遅くなりFoo1ます。
  • ループの順序を入れ替えても違いはありません。
  • 結果は、sun と openjdk の両方の JVM で java6 を使用して検証されました。

コード

public class CompositionTest {

    private static interface DoesBar {
        public void bar();
        public int count();
        public void count(int c);
    }

    private static final class Foo1 implements DoesBar {
        private int count = 0;
        public final void bar() { ++count; }
        public int count() { return count; }
        public void count(int c) { count = c; }
    }

    private static final class Foo2 implements DoesBar {
        private DoesBar bar;
        public Foo2(DoesBar bar) { this.bar = bar; }
        public final void bar() { bar.bar(); }
        public int count() { return bar.count(); }
        public void count(int c) { bar.count(c); }
    }

    public static void main(String[] args) {
        long time = 0;
        DoesBar bar = null;
        int reps = 100000000;

        for (int loop = 0; loop < 10; loop++) {
            bar = new Foo1();
            bar.count(0);

            int i = reps;
            time = System.nanoTime();
            while (i-- > 0) bar.bar();
            time = System.nanoTime() - time;

            if (reps != bar.count())
                throw new Error("reps != bar.count()");
        }
        System.out.println("Foo1 time: " + time);

        for (int loop = 0; loop < 10; loop++) {
            bar = new Foo2(new Foo1());
            bar.count(0);

            int i = reps;
            time = System.nanoTime();
            while (i-- > 0) bar.bar();
            time = System.nanoTime() - time;

            if (reps != bar.count())
                throw new Error("reps != bar.count()");
        }
        System.out.println("Foo2 time: " + time);
    }
}
4

2 に答える 2

2

あなたのマイクロベンチマークは無意味です。私のコンピューターでは、コードはループごとに約 8 ミリ秒で実行されます...意味のある数値を取得するには、ベンチマークを少なくとも 1 秒間実行する必要があります。

両方を約1秒間実行すると(ヒント、繰り返し以上が必要ですInteger.MAX_VALUE)、両方の実行時間は同じであることがわかります。

これはおそらく、JIT コンパイラーが間接化が無意味であることに気付き、両方のループで実行されるコードが同一になるように最適化 (または少なくともメソッド呼び出しをインライン化) したためです。

これを行うことができるのはbar、 inFoo2が実質的に final であることを知っているためです。また、コンストラクターへの引数Foo2が常に a になることも知っているためですFoo1(少なくとも、この小さなテストでは)。こうすることで、Foo2.barが呼び出されたときに正確なコード パスを知ることができます。また、このループが何度も実行されることも認識しています (実際には、ループが実行される回数を正確に認識しています)。そのため、コードをインライン化することをお勧めします。

それが正確に何をしているのかはわかりませんが、これらはすべて、JIT がコードについて行うことができる論理的な観察です。将来的には、一部の JIT コンパイラーが while ループ全体を最適化し、count を単に reps に設定することさえあるかもしれませんが、それはややありそうにありません。

于 2012-05-18T16:22:33.303 に答える
1

現代の言語でパフォーマンスを予測しようとしても、あまり生産的ではありません。

JVM は、一般的で読みやすい構造のパフォーマンスを向上させるために常に変更されていますが、これとは対照的に、一般的でなく扱いにくいコードは遅くなります。

できる限り明確にコードを記述してください。その後、コードが遅すぎて記述された仕様に合格できないと実際に識別されたポイントを実際に特定した場合は、いくつかの領域を手動で微調整する必要がある場合がありますが、これにはおそらく大規模な、オブジェクト キャッシュ、JVM オプションの微調整、本当にばかげた/間違ったコードの排除などの単純なアイデア (間違ったデータ構造は巨大になる可能性があります。私はかつて ArrayList を LinkedList に変更し、操作を 10 分から 5 秒に短縮し、ping 操作をマルチスレッド化しました。クラス B ネットワークの操作に 8 時間以上かかっていたことが発見され、数分になりました)。

于 2012-05-18T15:21:56.990 に答える