Kinerja Refleksi Java

Jawaban:

169

Ya, tentu saja. Melihat ke atas kelas melalui refleksi adalah, dengan besarnya , lebih mahal.

Mengutip dokumentasi Java tentang refleksi :

Karena refleksi melibatkan jenis yang diselesaikan secara dinamis, optimasi mesin virtual Java tertentu tidak dapat dilakukan. Akibatnya, operasi reflektif memiliki kinerja lebih lambat daripada rekan non-reflektif mereka, dan harus dihindari di bagian kode yang sering disebut dalam aplikasi yang sensitif terhadap kinerja.

Inilah tes sederhana yang saya peretas dalam 5 menit pada mesin saya, menjalankan Sun JRE 6u10:

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

Dengan hasil ini:

35 // no reflection
465 // using reflection

Ingatlah pencarian dan instantiasi dilakukan bersama-sama, dan dalam beberapa kasus pencarian dapat dihilangkan, tetapi ini hanyalah contoh dasar.

Bahkan jika Anda hanya instantiate, Anda masih mendapatkan kinerja yang baik:

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

Sekali lagi, YMMV.

Yuval Adam
sumber
5
Di komputer saya, panggilan .newInstance () hanya dengan satu panggilan Class.forName () bernilai 30 atau lebih. Tergantung pada versi VM, perbedaannya mungkin lebih dekat daripada yang Anda pikirkan dengan strategi caching yang tepat.
Sean Reilly
56
@Peter Lawrey di bawah menunjukkan bahwa tes ini benar-benar tidak valid karena kompiler mengoptimalkan solusi non-reflektif (Bahkan dapat membuktikan bahwa tidak ada yang dilakukan dan mengoptimalkan loop for). Perlu dikerjakan ulang dan mungkin harus dihapus dari SO sebagai informasi yang buruk / menyesatkan. Cache objek yang dibuat dalam array dalam kedua kasus untuk mencegah pengoptimal mengoptimalkannya. (Tidak dapat melakukan ini dalam situasi reflektif karena tidak dapat membuktikan bahwa konstruktor tidak memiliki efek samping)
Bill K
6
@ Bill K - jangan terbawa suasana. Ya, angkanya tidak aktif karena optimasi. Tidak, tes tidak sepenuhnya tidak valid. Saya menambahkan panggilan yang menghilangkan kemungkinan hasil miring, dan angka-angka masih ditumpuk terhadap refleksi. Bagaimanapun, ingatlah bahwa ini adalah patokan mikro yang sangat kasar yang hanya menunjukkan bahwa pantulan selalu menimbulkan overhead tertentu
Yuval Adam
4
Ini mungkin tolok ukur yang tidak berguna. Tergantung pada apa yang dilakukan sesuatu. Jika tidak ada efek samping yang terlihat, maka tolok ukur Anda hanya menjalankan kode mati.
nes1983
9
Saya baru saja menyaksikan JVM mengoptimalkan refleksi 35 kali lipat. Menjalankan tes berulang kali dalam satu lingkaran adalah cara Anda menguji kode yang dioptimalkan. Iterasi pertama: 3045ms, iterasi kedua: 2941ms, iterasi ketiga: 90ms, iterasi keempat: 83ms. Kode: c.newInstance (i). c adalah Konstruktor. Kode non-reflektif: A (i) baru, yang menghasilkan 13, 4, 3 .. ms kali. Jadi ya, refleksi dalam kasus ini lambat, tetapi tidak lebih lambat dari apa yang orang simpulkan, karena setiap tes yang saya lihat, mereka hanya menjalankan tes sekali tanpa memberikan JVM kesempatan untuk mengganti kode byte dengan mesin kode.
Mike
87

Ya, ini lebih lambat.

Tapi ingat aturan # 1 sialan - OPTIMASI PREMATUR ADALAH ROOT DARI SEMUA JAHAT

(Yah, mungkin diikat dengan # 1 untuk KERING)

Aku bersumpah, jika seseorang mendatangiku di tempat kerja dan menanyakan ini padaku, aku akan sangat berhati-hati terhadap kode mereka selama beberapa bulan ke depan.

Anda tidak boleh mengoptimalkan sampai Anda yakin Anda membutuhkannya, sampai saat itu, cukup tulis kode yang baik dan mudah dibaca.

Oh, dan aku juga tidak bermaksud menulis kode bodoh. Hanya memikirkan cara terbersih yang dapat Anda lakukan - tidak ada salin dan tempel, dll. (Tetap waspada terhadap hal-hal seperti loop batin dan menggunakan koleksi yang paling sesuai dengan kebutuhan Anda - Mengabaikan ini bukan pemrograman "tidak dioptimalkan" , ini pemrograman "buruk")

Ini membuat saya takut ketika saya mendengar pertanyaan seperti ini, tetapi kemudian saya lupa bahwa setiap orang harus mempelajari semua peraturan sendiri sebelum mereka benar-benar mendapatkannya. Anda akan mendapatkannya setelah menghabiskan satu bulan bulan men-debug sesuatu yang "Dioptimalkan" oleh seseorang.

EDIT:

Hal yang menarik terjadi di utas ini. Periksa jawaban # 1, ini adalah contoh seberapa kuat kompiler dalam mengoptimalkan berbagai hal. Tes ini sepenuhnya tidak valid karena instantiasi non-reflektif dapat sepenuhnya difaktorkan.

Pelajaran? Jangan pernah optimalkan sampai Anda telah menulis solusi kode yang bersih dan rapi dan membuktikannya terlalu lambat.

Bill K.
sumber
28
Saya sepenuhnya setuju dengan sentimen dari respons ini, namun jika Anda akan memulai keputusan desain utama, itu membantu untuk memiliki ide tentang kinerja sehingga Anda tidak pergi pada jalur yang sama sekali tidak bisa dijalankan. Mungkin dia hanya melakukan uji tuntas?
Limbic System
26
-1: Menghindari melakukan sesuatu dengan cara yang salah bukanlah optimasi, itu hanya melakukan hal-hal. Optimalisasi adalah melakukan sesuatu dengan cara yang salah, rumit karena masalah kinerja nyata atau imajiner.
soru
5
@ Atau Anda sepenuhnya setuju. Memilih daftar yang ditautkan dari daftar larik untuk jenis penyisipan adalah cara yang tepat untuk melakukan sesuatu. Tapi pertanyaan khusus ini - ada kasus penggunaan yang baik untuk kedua sisi dari pertanyaan awal, jadi memilih salah satu berdasarkan kinerja daripada solusi yang paling dapat digunakan akan salah. Saya tidak yakin kami tidak setuju sama sekali, jadi saya tidak yakin mengapa Anda mengatakan "-1".
Bill K
14
Setiap programmer analis yang masuk akal perlu mempertimbangkan efisiensi pada tahap awal atau Anda mungkin berakhir dengan sistem yang TIDAK dapat dioptimalkan dalam jangka waktu yang efisien dan berharga. Tidak, Anda tidak mengoptimalkan setiap siklus jam tetapi Anda pasti menggunakan praktik terbaik untuk sesuatu yang mendasar seperti instantiasi kelas. Contoh ini bagus untuk MENGAPA Anda mempertimbangkan pertanyaan-pertanyaan semacam itu mengenai refleksi. Itu akan menjadi programmer yang sangat miskin yang pergi ke depan dan menggunakan refleksi di seluruh sistem sejuta baris hanya untuk kemudian menemukan itu adalah urutan besarnya terlalu lambat.
RichieHH
2
@Richard Riley. Umumnya instantiasi kelas adalah peristiwa yang cukup langka untuk kelas yang dipilih Anda akan menggunakan refleksi. Saya kira Anda benar - beberapa orang mungkin instantiate setiap kelas secara reflektif, bahkan yang dibuat ulang secara konstan. Saya akan menyebut pemrograman yang sangat buruk (meskipun itu pun Anda BISA mengimplementasikan cache instance kelas untuk digunakan kembali setelah fakta dan tidak terlalu merusak kode Anda - jadi saya kira saya masih akan mengatakan SELALU desain untuk keterbacaan, kemudian profil dan optimalkan nanti)
Bill K
36

Anda mungkin menemukan bahwa A a = new A () sedang dioptimalkan oleh JVM. Jika Anda meletakkan objek ke dalam array, mereka tidak berkinerja baik. ;) Cetakan berikut ini ...

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

Ini menunjukkan perbedaannya adalah sekitar 150 ns pada mesin saya.

Peter Lawrey
sumber
jadi Anda baru saja membunuh pengoptimal, jadi sekarang kedua versi lambat. Refleksi, oleh karena itu, masih sangat lambat.
gbjbaanb
13
@ gbjbaanb jika optimizer mengoptimalkan pembuatan itu sendiri maka itu bukan tes yang valid. Oleh karena itu uji @ Peter valid karena benar-benar membandingkan waktu pembuatan (Pengoptimal tidak akan dapat bekerja dalam situasi dunia nyata APAPUN karena dalam situasi dunia nyata Anda memerlukan objek yang Anda instantiasikan).
Bill K
10
@ nes1983 Dalam hal ini Anda bisa mengambil kesempatan untuk membuat tolok ukur yang lebih baik. Mungkin Anda dapat menawarkan sesuatu yang konstruktif, seperti apa yang seharusnya ada di tubuh metode ini.
Peter Lawrey
1
pada mac saya, openjdk 7u4, perbedaannya adalah 95ns versus 100ns. Alih-alih menyimpan A dalam array, saya menyimpan kode hash. Jika Anda mengatakan -verbose: class Anda dapat melihat kapan hotspot menghasilkan bytecode untuk membangun A dan speedup yang menyertainya.
Ron
@ PeterLawrey Jika saya mencari sekali (satu panggilan ke Class.getDeclaredMethod) dan kemudian menelepon Method.invokebeberapa kali? Apakah saya menggunakan refleksi sekali atau sebanyak yang saya minta? Pertanyaan lanjutan, bagaimana jika alih-alih Methoditu adalah Constructordan saya lakukan Constructor.newInstancebeberapa kali?
tmj
28

Jika memang ada kebutuhan untuk sesuatu yang lebih cepat daripada refleksi, dan itu bukan hanya optimasi prematur, maka bytecode generasi dengan ASM atau perpustakaan tingkat yang lebih tinggi adalah sebuah pilihan. Menghasilkan bytecode pertama kali lebih lambat daripada hanya menggunakan refleksi, tetapi begitu bytecode dibuat, itu secepat kode Java normal dan akan dioptimalkan oleh kompiler JIT.

Beberapa contoh aplikasi yang menggunakan pembuatan kode:

  • Meminta metode pada proksi yang dihasilkan oleh CGLIB sedikit lebih cepat dari proksi dinamis Java , karena CGLIB menghasilkan bytecode untuk prokinya, tetapi proksi dinamis hanya menggunakan refleksi ( saya mengukur CGLIB sekitar 10x lebih cepat dalam pemanggilan metode, tetapi membuat proksi lebih lambat).

  • JSerial menghasilkan bytecode untuk membaca / menulis bidang objek serial, alih-alih menggunakan refleksi. Ada beberapa tolok ukur di situs JSerial.

  • Saya tidak 100% yakin (dan saya tidak ingin membaca sumbernya sekarang), tetapi saya pikir Guice menghasilkan bytecode untuk melakukan injeksi ketergantungan. Koreksi saya jika saya salah.

Esko Luontola
sumber
27

"Signifikan" sepenuhnya tergantung pada konteks.

Jika Anda menggunakan refleksi untuk membuat objek handler tunggal berdasarkan pada beberapa file konfigurasi, dan kemudian menghabiskan sisa waktu Anda menjalankan query database, maka itu tidak signifikan. Jika Anda membuat banyak objek melalui refleksi dalam satu lingkaran ketat, maka ya, itu penting.

Secara umum, fleksibilitas desain (jika diperlukan!) Harus mendorong Anda menggunakan refleksi, bukan kinerja. Namun, untuk menentukan apakah kinerja merupakan masalah, Anda perlu membuat profil alih-alih mendapatkan respons sewenang-wenang dari forum diskusi.

kdgregory
sumber
24

Ada beberapa overhead dengan refleksi, tetapi jauh lebih kecil pada VM modern daripada sebelumnya.

Jika Anda menggunakan refleksi untuk membuat setiap objek sederhana di program Anda, maka ada sesuatu yang salah. Menggunakannya sesekali, ketika Anda memiliki alasan yang baik, seharusnya tidak menjadi masalah sama sekali.

Marcus Downing
sumber
11

Ya ada hit kinerja saat menggunakan Refleksi tetapi solusi yang mungkin untuk optimasi adalah dengan metode caching:

  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");

akan menghasilkan:

[java] Metode panggilan 10.000 kali secara refleks dengan pencarian membutuhkan 5.618 mili

[java] Metode pemanggilan 1000000 kali secara refleks dengan cache membutuhkan 270 millis

mel3kings
sumber
Metode / konstruktor penggunaan kembali memang berguna dan membantu, tetapi perhatikan bahwa tes di atas tidak memberikan angka yang berarti karena masalah pembandingan biasa (tidak ada pemanasan, jadi loop pertama khususnya sebagian besar mengukur waktu pemanasan JVM / JIT).
StaxMan
7

Refleksi lambat, meskipun alokasi objek tidak seputus harapannya seperti aspek refleksi lainnya. Untuk mencapai kinerja yang setara dengan instantiasi berbasis refleksi mengharuskan Anda untuk menulis kode Anda sehingga jit dapat mengetahui kelas mana yang sedang dipakai. Jika identitas kelas tidak dapat ditentukan, maka kode alokasi tidak dapat digarisbawahi. Lebih buruk, analisis pelarian gagal, dan objek tidak dapat dialokasikan stack. Jika Anda beruntung, profil run-time JVM mungkin datang untuk menyelamatkan jika kode ini menjadi panas, dan dapat menentukan secara dinamis kelas mana yang mendominasi dan dapat mengoptimalkan untuk yang itu.

Ketahuilah bahwa microbenchmark pada utas ini sangat cacat, jadi bawalah mereka dengan sebutir garam. Yang paling tidak cacat sejauh ini adalah Peter Lawrey: ia melakukan pemanasan untuk mendapatkan metode yang tepat, dan (secara sadar) mengalahkan analisis pelarian untuk memastikan alokasi benar-benar terjadi. Meskipun ada masalah, misalnya: misalnya, sejumlah besar toko array dapat diperkirakan mengalahkan cache dan menyimpan buffer, jadi ini akan menjadi benchmark memori jika alokasi Anda sangat cepat. (Kudos kepada Peter karena mendapatkan kesimpulan yang tepat: bahwa perbedaannya adalah "150ns" daripada "2.5x". Saya curiga dia melakukan hal semacam ini untuk mencari nafkah.)

Doradus
sumber
7

Cukup menarik, dengan menetapkan setAccessible (true), yang melewatkan pemeriksaan keamanan, memiliki pengurangan 20% dalam biaya.

Tanpa setAccessible (true)

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

Dengan setAccessible (true)

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns
Mikhail Kraizman
sumber
1
Tampak jelas bagi saya pada prinsipnya. Apakah angka-angka ini berskala linear, saat menjalankan 1000000doa?
Lukas Eder
Sebenarnya setAccessible()dapat memiliki banyak perbedaan secara umum, terutama untuk metode dengan banyak argumen, jadi harus selalu disebut.
StaxMan
6

Ya, ini jauh lebih lambat. Kami menjalankan beberapa kode yang melakukan itu, dan sementara saya tidak memiliki metrik yang tersedia saat ini, hasil akhirnya adalah kami harus memperbaiki kode itu untuk tidak menggunakan refleksi. Jika Anda tahu apa kelasnya, cukup hubungi konstruktor secara langsung.

Elie
sumber
1
+1 Saya punya pengalaman serupa. Adalah baik untuk memastikan hanya menggunakan refleksi jika itu benar-benar diperlukan.
Ryan Thames
mis. perpustakaan berbasis AOP memang perlu refleksi.
gaurav
4

Dalam doReflection () adalah overhead karena Class.forName ("misc.A") (yang akan membutuhkan pencarian kelas, berpotensi memindai jalur kelas pada filsystem), daripada newInstance () dipanggil pada kelas. Saya bertanya-tanya seperti apa statistiknya jika Class.forName ("misc.A") dilakukan hanya sekali di luar for-loop, itu tidak benar-benar harus dilakukan untuk setiap doa dari loop.

tikoo
sumber
1

Ya, selalu akan lebih lambat membuat objek dengan refleksi karena JVM tidak dapat mengoptimalkan kode pada waktu kompilasi. Lihat tutorial Sun / Java Reflection untuk lebih jelasnya.

Lihat tes sederhana ini:

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");
    }
}
aledbf
sumber
3
Perhatikan bahwa Anda harus memisahkan lookup ( Class.forName()) dari instanciation (newInstance ()), karena mereka bervariasi secara signifikan dalam karakteristik kinerja mereka dan Anda kadang-kadang dapat menghindari pencarian berulang dalam sistem yang dirancang dengan baik.
Joachim Sauer
3
Juga: Anda perlu menjalankan setiap tugas berkali-kali untuk mendapatkan tolok ukur yang berguna: pertama-tama tindakan terlalu lambat untuk diukur dengan andal dan kedua Anda harus menghangatkan HotSpot VM untuk mendapatkan angka yang bermanfaat.
Joachim Sauer
1

Seringkali Anda dapat menggunakan Apache commons BeanUtils atau PropertyUtils yang melakukan introspeksi (pada dasarnya mereka men-cache data meta tentang kelas sehingga mereka tidak selalu perlu menggunakan refleksi).

sproketboy
sumber
0

Saya pikir itu tergantung pada seberapa ringan / berat metode targetnya. jika metode target sangat ringan (mis. pengambil / penyetel), itu bisa 1 ~ 3 kali lebih lambat. jika metode target memakan waktu sekitar 1 milidetik atau lebih, maka kinerjanya akan sangat dekat. di sini adalah tes yang saya lakukan dengan Java 8 dan 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;
        }
    }
}

Kode uji lengkap tersedia di GitHub: ReflectionTest.java

user_3380739
sumber