web-dev-qa-db-ja.com

Javaリフレクション:なぜそんなに遅いのですか?

私は常に回避してきましたJavaスローの評判に基づいて、リフレクションソレイ。現在のプロジェクトの設計で、それを使用できるとコードがより読みやすくエレガントになるという点に達しました、それで私はそれをやってみることにしました。

違いに驚いただけで、実行時間がほぼ100倍長いことに気づきました。空のクラスをインスタンス化するだけのこの単純な例でも、信じられないほどです。

class B {

}

public class Test {

    public static long timeDiff(long old) {
        return System.currentTimeMillis() - old;
    }

    public static void main(String args[]) throws Exception {

        long numTrials = (long) Math.pow(10, 7);

        long millis;

        millis = System.currentTimeMillis();

        for (int i=0; i<numTrials; i++) {
            new B();
        }
        System.out.println("Normal instaniation took: "
                 + timeDiff(millis) + "ms");

        millis = System.currentTimeMillis();

        Class<B> c = B.class;

        for (int i=0; i<numTrials; i++) {
            c.newInstance();
        }

        System.out.println("Reflecting instantiation took:" 
              + timeDiff(millis) + "ms");

    }
}

だから本当に、私の質問は

  • なぜこれが遅いのですか?私が間違っていることはありますか? (上の例でも違いを示しています)。通常のインスタンス化よりも実際に100倍遅くなるとは信じられません。

  • コードをデータとして扱うために使用できる他の方法はありますか(今のところJava)で立ち往生していることに注意してください)

46
Mike

テストに欠陥がある可能性があります。 通常、JVMは通常のインスタンス化を最適化する可能性がありますが、リフレクトユースケースの最適化を行うことができませんでした

時間とは何かを不思議に思っている人のために、ウォームアップフェーズを追加し、作成したオブジェクトを維持するために配列を使用しました(実際のプログラムの動作に似ています)。私は自分のOSX、jdk7システムでテストコードを実行し、これを取得しました。

インスタンス化の反映にかかった時間:5180ms
通常のインスタンス化にかかった時間:2001ミリ秒

変更されたテスト:

public class Test {

    static class B {

    }

    public static long timeDiff(long old) {
        return System.nanoTime() - old;
    }

    public static void main(String args[]) throws Exception {

        int numTrials = 10000000;
        B[] bees = new B[numTrials];
        Class<B> c = B.class;
        for (int i = 0; i < numTrials; i++) {
            bees[i] = c.newInstance();
        }
        for (int i = 0; i < numTrials; i++) {
            bees[i] = new B();
        }

        long nanos;

        nanos = System.nanoTime();
        for (int i = 0; i < numTrials; i++) {
            bees[i] = c.newInstance();
        }
        System.out.println("Reflecting instantiation took:" + TimeUnit.NANOSECONDS.toMillis(timeDiff(nanos)) + "ms");

        nanos = System.nanoTime();
        for (int i = 0; i < numTrials; i++) {
            bees[i] = new B();
        }
        System.out.println("Normal instaniation took: " + TimeUnit.NANOSECONDS.toMillis(timeDiff(nanos)) + "ms");
    }


}
40
Tim Bender

いくつかの明らかな理由で反射が遅い:

  1. コンパイラは、何をしているのかについて実際の考えを持てないため、最適化を行うことはできません。これはおそらくJITにも当てはまります
  2. 呼び出される/作成されるものはすべてdiscoveredでなければなりません(つまり、クラスは名前で検索され、メソッドは一致のために検索されます)
  3. 引数は、ボクシング/アンボクシング、配列へのパッキング、Exceptions _InvocationTargetExceptionsへのラップ、再スローなどによってドレスアップする必要があります。
  4. Jon Skeetがここで言及する のすべての処理。

何かが100倍遅いからといって、リフレクションがプログラムを設計するための「正しい方法」であると想定して、それがあなたにとって遅すぎるとは限りません。たとえば、IDEはリフレクションを多用していると思いますが、パフォーマンスの観点から見ると、IDEはほとんど問題ありません。

結局のところ、反射のオーバーヘッドは、無意味に変換される可能性が高いと比較して、たとえば、XMLを解析するか、データベースにアクセスする

覚えておくべきもう1つのポイントは、マイクロベンチマークは、何かが実際にどれほど高速であるかを判断するための悪名高い欠陥メカニズムであるということですTim Bender'sremarks と同様に、JVMは「ウォームアップ」に時間がかかり、JITはオンザフライなどでコードのホットスポットを再最適化できます。 。

42
oxbow_lakes

Bをインスタンス化するためのJITされたコードは、信じられないほど軽量です。基本的には、十分なメモリを割り当てる必要があり(GCが必要でない限り、ポインタをインクリメントするだけです)、それだけです。実際に呼び出すコンストラクタコードはありません。 JITがそれをスキップするかどうかはわかりませんが、どちらにしても、それほど多くのことはありません。

これを、リフレクションが行うすべてのことと比較してください。

  • パラメータのないコンストラクタがあることを確認します
  • パラメータなしのコンストラクタのアクセシビリティを確認する
  • 発信者がリフレクションを使用するためのアクセス権を持っていることを確認してください
  • (実行時に)割り当てる必要があるスペースの量を計算する
  • コンストラクターコードを呼び出す(コンストラクターが空であることを事前に認識していないため)

...そしておそらく私が考えもしなかった他の事柄。

通常、リフレクションはパフォーマンスが重要なコンテキストでは使用されません。そのような動的な動作が必要な場合は、代わりに [〜#〜] bcel [〜#〜] のようなものを使用できます。

27
Jon Skeet

コンストラクタをアクセス可能にすると、実行速度が大幅に向上するようです。現在、他のバージョンよりも10〜20倍遅いだけです。

    Constructor<B> c = B.class.getDeclaredConstructor();
    c.setAccessible(true);
    for (int i = 0; i < numTrials; i++) {
        c.newInstance();
    }

Normal instaniation took: 47ms
Reflecting instantiation took:718ms

また、サーバーVMを使用すると、さらに最適化できるため、速度が3〜4倍遅くなります。これは非常に典型的なパフォーマンスです。 記事 Geoがリンクしていることは良い読み物です。

Normal instaniation took: 47ms
Reflecting instantiation took:140ms

ただし、-XX:+ DoEscapeAnalysisを使用してスカラー置換を有効にすると、JVMは通常のインスタンス化を最適化できます(0-15msになります)が、反射インスタンス化は同じままです。

10
Esko Luontola
  • リフレクションは最初に導入されたときは非常に低速でしたが、新しいJREではかなり高速になりました
  • それでも、内部ループでリフレクションを使用することは良い考えではないかもしれません
  • リフレクションベースのコードは、JITベースの最適化の可能性が低い
  • リフレクションは主に、疎結合コンポーネントの接続、つまり具体的なクラスとメソッドの検索に使用されます。この場合、インターフェースのみがわかっています。依存性注入フレームワーク、JDBC実装クラスまたはXMLパーサーのインスタンス化です。これらの使用法は、多くの場合、システムの起動時に1回だけ実行できるため、多少の非効率性は問題になりません。
3
mfx

@Tim Benderのコードは、私のマシン(jdk_1.8_45、os_x 10.10、i7、16G)でこれらの結果を提供します:

Reflecting instantiation took:1139ms Normal instaniation took: 4969ms

最新のJVMのようですが、リフレクションコードも最適化されます。

0
bob