Javaリフレクションのパフォーマンス


回答:


169

そのとおり。リフレクションを介してクラスを検索すると、規模が大きくなるため、コストが高くなります。

リフレクションに関するJavaのドキュメントを引用:

リフレクションには動的に解決されるタイプが含まれるため、特定のJava仮想マシンの最適化を実行できません。その結果、リフレクティブ操作は非リフレクティブ操作よりもパフォーマンスが遅いため、パフォーマンスの影響を受けやすいアプリケーションで頻繁に呼び出されるコードのセクションでは回避する必要があります。

これは、Sun JRE 6u10を実行している私のマシンで5分でハッキングした簡単なテストです。

public class Main {

    public static void main(String[] args) throws Exception
    {
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = new A();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }

    public static void doReflection() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = (A) Class.forName("misc.A").newInstance();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }
}

これらの結果:

35 // no reflection
465 // using reflection

ルックアップとインスタンス化は一緒に行われ、場合によってはルックアップをリファクタリングできますが、これは基本的な例にすぎません。

インスタンス化しただけでも、パフォーマンスに影響があります。

30 // no reflection
47 // reflection using one lookup, only instantiating

再び、YMMV。


5
私のマシンでは、Class.forName()呼び出しが1つだけの.newInstance()呼び出しのスコアは30程度です。VMのバージョンによっては、適切なキャッシング戦略で考えるよりも差が近い場合があります。
Sean Reilly、

56
以下の@Peter Lawreyは、コンパイラーが非反射ソリューションを最適化していたため、このテストは完全に無効であると指摘しました(何も行われていないことを証明し、forループを最適化することもできます)。やり直す必要があり、おそらく悪い/誤解を招く情報としてSOから削除する必要があります。どちらの場合も、作成されたオブジェクトを配列にキャッシュして、オプティマイザが最適化しないようにします。(コンストラクターに副作用がないことを証明できないため、反射的な状況ではこれを行うことはできません)
Bill K

6
@Bill K-夢中にならないようにしましょう。はい、最適化により数値はずれています。いいえ、テストはありません完全に無効。結果を歪める可能性を排除する呼び出しを追加しましたが、数字は反射に対して積み重ねられています。いずれにせよ、これは非常に粗雑なマイクロベンチマークであることを思い出してください。これは、反射が常に特定のオーバーヘッドを招くことを示しているだけです
Yuval Adam

4
これはおそらく役に立たないベンチマークです。何をするかによります。目に見える副作用が何もない場合、ベンチマークはデッドコードのみを実行します。
nes1983

9
JVMのリフレクションを35倍に最適化するのを見たところです。ループ内で繰り返しテストを実行すると、最適化されたコードをテストできます。最初の反復:3045ms、2番目の反復:2941ms、3番目の反復:90ms、4番目の反復:83ms。コード:c.newInstance(i)。cはコンストラクタです。非反射コード:13、4、3 .. ms時間を生成する新しいA(i)。つまり、この場合のリフレクションは低速でしたが、人々が結論付けているものほど遅くはありませんでした。私が見ているすべてのテストは、JVMにバイトコードをマシンに置き換える機会を与えずに、テストを1回実行するだけだからです。コード。
Mike

87

はい、遅いです。

しかし、いまいましい#1ルールを忘れないでください-永続的な最適化はすべての悪の根源です

(まあ、DRYの場合は#1と結びつくかもしれません)

誰かが仕事で私に近づいてきて、これについて私に尋ねたら、私は次の数か月の間彼らのコードに非常に注意を払うだろうと私は誓います。

それが必要であると確信するまで、最適化してはいけません。それまでは、適切で読みやすいコードを記述してください。

ああ、私も愚かなコードを書くつもりはありません。可能な限り最もクリーンな方法を考えてください-コピーや貼り付けなどはしないでください(内部ループのようなものや、ニーズに最適なコレクションを使用することに注意してください-これらを無視することは、「最適化されていない」プログラミングではありません。 、それは「悪い」プログラミングです)

このような質問を聞くと、私はびっくりしますが、実際にすべてのルールを理解する前に、すべてのルールを自分で学習する必要があることを忘れています。誰かが「最適化された」何かをデバッグするのに1か月を費やした後に、それを得るでしょう。

編集:

このスレッドで興味深いことが起こりました。#1の回答を確認してください。これは、コンパイラが物事を最適化する上でどれほど強力であるかの例です。非反射インスタンス化は完全に除外できるため、テストは完全に無効です。

レッスン?クリーンできれいにコード化されたソリューションを作成し、それが遅すぎることが判明するまで、決して最適化しないでください。


28
私はこの応答の感情に完全に同意しますが、大きな設計上の決定に着手しようとしている場合は、パフォーマンスについてのアイデアを持っているので、完全に機能しないパスを実行しないでください。たぶん彼はデューデリジェンスをしているだけなのでしょうか?
Limbicシステム

26
-1:物事を間違った方法で回避することは最適化ではなく、単に物事を行うことです。最適化は、実際のまたは架空のパフォーマンスの問題により、間違った複雑な方法で物事を行っています。
soru

5
@soruは完全に同意します。挿入ソートのために配列リストよりもリンクされたリストを選択することは、単純に正しい方法です。しかし、この特定の質問-元の質問の両側には良いユースケースがあるため、最も使いやすいソリューションではなく、パフォーマンスに基づいて選択するのは間違っています。私たちがまったく同意していないのかわからないので、なぜ「-1」と言ったのかわかりません。
ビルK

14
賢明なアナリストプログラマーは、早い段階で効率を考慮する必要があります。そうしないと、効率的でコストに見合う時間枠で最適化できないシステムになってしまう可能性があります。いいえ、すべてのクロックサイクルを最適化するわけではありませんが、クラスのインスタンス化などの基本的なものに対しては、ベストプラクティスを確実に採用しています。この例は、反射についてそのような質問を検討する理由の1つです。100万行のシステム全体でリフレクションを使用して、後で桁違いに遅すぎることに気づいたのは、かなり貧しいプログラマーでしょう。
RichieHH 2010

2
@Richard Riley一般に、クラスのインスタンス化は、リフレクションを使用する選択したクラスのかなりまれなイベントです。しかし、あなたが正しいと思います-一部の人々はすべてのクラスを反射的にインスタンス化するかもしれません。私はそれをかなり悪いプログラミングと呼んでいます(それでも、事後の再利用のためにクラスインスタンスのキャッシュを実装でき、コードにあまり害を与えないでください-したがって、読みやすさを考慮して常に設計し、プロファイルを作成して最適化すると思います後で)
Bill Kが

36

A a = new A()がJVMによって最適化されていることがわかります。オブジェクトを配列に入れると、それらはあまりうまく機能しません。;)次のプリント...

new A(), 141 ns
A.class.newInstance(), 266 ns
new A(), 103 ns
A.class.newInstance(), 261 ns

public class Run {
    private static final int RUNS = 3000000;

    public static class A {
    }

    public static void main(String[] args) throws Exception {
        doRegular();
        doReflection();
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = new A();
        }
        System.out.printf("new A(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }

    public static void doReflection() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = A.class.newInstance();
        }
        System.out.printf("A.class.newInstance(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }
}

これは、私のマシンでの差が約150 nsであることを示唆しています。


オプティマイザを強制終了したので、どちらのバージョンも遅くなります。したがって、反射はまだ遅いです。
gbjbaanb 2009

13
@gbjbaanbオプティマイザが作成自体を最適化していた場合、それは有効なテストではありませんでした。@Peterのテストは、実際に作成時間を比較するため有効です(現実世界の状況では、インスタンス化するオブジェクトが必要になるため、オプティマイザは現実世界の状況では機能しません)。
ビルK

10
@ nes1983その場合、あなたはより良いベンチマークを作成する機会を得たかもしれません。おそらく、メソッドの本体にあるべきものなど、建設的なものを提供できます。
Peter Lawrey、

1
私のMac、openjdk 7u4では、100nsと95nsの違いがあります。配列にAを格納する代わりに、hashCodeを格納します。-verbose:classと言うと、ホットスポットがAを構築するためのバイトコードを生成するタイミングと、それに伴うスピードアップを確認できます。
ロン・

@PeterLawrey 1回検索して(を1 回Class.getDeclaredMethod呼び出す)、次にMethod.invoke複数回呼び出す場合 リフレクションを1回使用しますか、それとも呼び出した回数だけ使用しますか?質問をフォローアップ、代わりにどのような場合にはMethod、それであるConstructorと私はConstructor.newInstance複数回?
tmj

28

リフレクションよりも速い何かが本当に必要であり、それが単なる時期尚早の最適化ではない場合は、ASMまたはより高レベルのライブラリーを使用したバイトコード生成がオプションです。初めてバイトコードを生成することは、リフレクションを使用するよりも遅くなりますが、バイトコードが生成されると、通常のJavaコードと同じくらい高速で、JITコンパイラーによって最適化されます。

コード生成を使用するアプリケーションの例:

  • CGLIBプロキシのバイトコードを生成するため、CGLIBによって生成されたプロキシでメソッドを呼び出すのは、Javaの動的プロキシよりもわずかに高速ですが、動的プロキシはリフレクションのみを使用します(CGLIBはメソッド呼び出しで約10倍速く測定されましたが、プロキシの作成は遅くなりました)。

  • JSerialは、リフレクションを使用する代わりに、シリアル化されたオブジェクトのフィールドを読み書きするためのバイトコードを生成します。JSerialのサイトにはいくつかのベンチマークがあります。

  • 私は100%確信はありません(そして、私はソースを読む気がしません)が、Guiceは依存関係注入を行うためのバイトコードを生成すると思います。私が間違っていたら訂正してください。


27

「重要」は完全にコンテキストに依存しています。

リフレクションを使用して、構成ファイルに基づいて単一のハンドラーオブジェクトを作成し、残りの時間をデータベースクエリの実行に費やしている場合、それは重要ではありません。タイトなループでリフレクションを介して多数のオブジェクトを作成している場合、そうです、それは重要です。

一般に、設計の柔軟性(必要な場合!)は、パフォーマンスではなくリフレクションの使用を促進する必要があります。ただし、パフォーマンスが問題であるかどうかを判断するには、ディスカッションフォーラムから任意の応答を受け取るのではなく、プロファイリングする必要があります。


24

リフレクションには多少のオーバーヘッドがありますが、最近のVMでは以前よりもはるかに小さくなっています。

リフレクションを使用してプログラム内のすべての単純なオブジェクトを作成している場合、何かがおかしいです。時々それを使うことは、あなたが正当な理由があるとき、まったく問題になるべきではありません。


11

はい、Reflectionを使用するとパフォーマンスが低下しますが、最適化の可能な回避策はメソッドをキャッシュすることです。

  Method md = null;     // Call while looking up the method at each iteration.
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md = ri.getClass( ).getMethod("getValue", null);
        md.invoke(ri, null);
      }

      System.out.println("Calling method " + CALL_AMOUNT+ " times reflexively with lookup took " + (System.currentTimeMillis( ) - millis) + " millis");



      // Call using a cache of the method.

      md = ri.getClass( ).getMethod("getValue", null);
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md.invoke(ri, null);
      }
      System.out.println("Calling method " + CALL_AMOUNT + " times reflexively with cache took " + (System.currentTimeMillis( ) - millis) + " millis");

結果は:

[java]ルックアップでメソッドを1000000回再帰的に呼び出すと5618ミリ秒かかりました

[java]キャッシュを使用してメソッドを1000000回再帰的に呼び出すと、270ミリ秒かかりました


メソッド/コンストラクターを再利用することは確かに便利で役立ちますが、通常のベンチマークの問題(ウォームアップがないため、特に最初のループは主にJVM / JITウォームアップ時間を測定しているため)のため、上記のテストでは意味のある数値が得られないことに注意してください。
StaxMan 2016

7

オブジェクトの割り当ては、リフレクションの他の側面ほど絶望的ではありませんが、リフレクションは低速です。リフレクションベースのインスタンス化で同等のパフォーマンスを実現するには、インスタンス化されているクラスをjitが認識できるようにコードを記述する必要があります。クラスのIDを判別できない場合、割り当てコードをインライン化できません。さらに悪いことに、エスケープ分析が失敗し、オブジェクトをスタックに割り当てることができません。運が良ければ、JVMのランタイムプロファイリングがこのコードが熱くなったときに助けになり、どのクラスが優勢であるかを動的に判断し、そのクラスに最適化することができます。

このスレッドのマイクロベンチマークには深い欠陥があることに注意してください。そのため、一粒の塩を用意してください。ピーター・ローリーの欠点はこれまでで最も少ないものです。これは、ウォームアップの実行によってメソッドを不安定にし、エスケープ分析を(意識的に)無効にして、割り当てが実際に行われていることを確認します。ただし、それには問題があります。たとえば、膨大な数の配列ストアは、キャッシュとストアバッファーを無効にすることが予想されるため、割り当てが非常に速い場合、これは主にメモリベンチマークになります。(しかし、結論を正しく理解することについてピーターに称賛:違いは「2.5x」ではなく「150ns」です。彼はこの種のことを生計のために行っているのではないかと思います。)


7

興味深いことに、セキュリティチェックをスキップするsetAccessible(true)を設定すると、コストが20%削減されます。

setAccessible(true)なし

new A(), 70 ns
A.class.newInstance(), 214 ns
new A(), 84 ns
A.class.newInstance(), 229 ns

setAccessible(true)を使用

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns

1
原則的に私には明らかなようです。1000000呼び出しを実行すると、これらの数値は線形にスケーリングしますか?
Lukas Eder

実際にsetAccessible()は、特に複数の引数を持つメソッドの場合、一般にはるかに大きな違いがある可能性があるため、常に呼び出す必要があります。
StaxMan 2016

6

はい、かなり遅くなります。そのためのコードを実行していましたが、現時点で使用できるメトリックスはありませんが、リフレクションを使用しないようにコードをリファクタリングする必要がありました。クラスがわかっている場合は、コンストラクタを直接呼び出します。


1
+1私も同じような経験をしました。絶対に必要な場合にのみ、リフレクションを使用するようにしてください。
ライアンテムズ

たとえば、AOPベースのライブラリはリフレクションを必要とします。
gaurav 2018年

4

doReflection()には、クラスで呼び出されるnewInstance()ではなく、Class.forName( "misc.A")(クラス検索が必要で、filsystemのクラスパスをスキャンする可能性がある)によるオーバーヘッドがあります。Class.forName( "misc.A")がforループの外で1回だけ実行される場合、統計はどのように見えるのだろうと思います。ループの呼び出しごとに実際に実行する必要はありません。


1

はい、JVMはコンパイル時にコードを最適化できないため、リフレクションによるオブジェクトの作成は常に遅くなります。詳細については、Sun / Java Reflectionチュートリアルを参照してください。

次の簡単なテストをご覧ください。

public class TestSpeed {
    public static void main(String[] args) {
        long startTime = System.nanoTime();
        Object instance = new TestSpeed();
        long endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");

        startTime = System.nanoTime();
        try {
            Object reflectionInstance = Class.forName("TestSpeed").newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");
    }
}

3
ルックアップ(Class.forName())をインスタンス化(newInstance())から分離する必要があることに注意してください。これらはパフォーマンス特性が大幅に異なり、適切に設計されたシステムでのルックアップの繰り返しを回避できる場合があるためです。
ヨアヒムザウアー

3
また、有用なベンチマークを取得するには、各タスクを何度も実行する必要があります。まず、アクションが遅すぎて信頼性の高い測定ができず、次に、有用な数値を取得するためにHotSpot VMをウォームアップする必要があります。
ヨアヒムザウアー

1

多くの場合、イントロスペクションを行うApacheコモンズのBeanUtilsまたはPropertyUtilsを使用できます(基本的に、クラスに関するメタデータをキャッシュするため、常にリフレクションを使用する必要はありません)。


0

それはターゲットメソッドがどれだけ軽い/重いかによります。ターゲットメソッドが非常に軽い場合(たとえば、getter / setter)、1〜3倍遅くなる可能性があります。ターゲットメソッドに約1ミリ秒以上かかる場合、パフォーマンスは非常に近くなります。ここに私がJava 8とreflectasmで行ったテストがあります:

public class ReflectionTest extends TestCase {    
    @Test
    public void test_perf() {
        Profiler.run(3, 100000, 3, "m_01 by refelct", () -> Reflection.on(X.class)._new().invoke("m_01")).printResult();    
        Profiler.run(3, 100000, 3, "m_01 direct call", () -> new X().m_01()).printResult();    
        Profiler.run(3, 100000, 3, "m_02 by refelct", () -> Reflection.on(X.class)._new().invoke("m_02")).printResult();    
        Profiler.run(3, 100000, 3, "m_02 direct call", () -> new X().m_02()).printResult();    
        Profiler.run(3, 100000, 3, "m_11 by refelct", () -> Reflection.on(X.class)._new().invoke("m_11")).printResult();    
        Profiler.run(3, 100000, 3, "m_11 direct call", () -> X.m_11()).printResult();    
        Profiler.run(3, 100000, 3, "m_12 by refelct", () -> Reflection.on(X.class)._new().invoke("m_12")).printResult();    
        Profiler.run(3, 100000, 3, "m_12 direct call", () -> X.m_12()).printResult();
    }

    public static class X {
        public long m_01() {
            return m_11();
        }    
        public long m_02() {
            return m_12();
        }    
        public static long m_11() {
            long sum = IntStream.range(0, 10).sum();
            assertEquals(45, sum);
            return sum;
        }    
        public static long m_12() {
            long sum = IntStream.range(0, 10000).sum();
            assertEquals(49995000, sum);
            return sum;
        }
    }
}

完全なテストコードはGitHubで入手できます:ReflectionTest.java

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.