Hiệu suất phản chiếu Java


Câu trả lời:


169

Chắc chắn rồi. Nhìn lên một lớp thông qua sự phản chiếu, theo độ lớn , đắt hơn.

Trích dẫn tài liệu của Java về sự phản ánh :

Vì sự phản chiếu liên quan đến các loại được giải quyết động, các tối ưu hóa máy ảo Java nhất định không thể được thực hiện. Do đó, các hoạt động phản chiếu có hiệu suất chậm hơn so với các đối tác không phản chiếu của chúng và nên tránh trong các phần của mã được gọi là thường xuyên trong các ứng dụng nhạy cảm với hiệu suất.

Đây là một thử nghiệm đơn giản tôi đã hack trong 5 phút trên máy của mình, chạy 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);
    }
}

Với những kết quả này:

35 // no reflection
465 // using reflection

Hãy nhớ rằng việc tra cứu và khởi tạo được thực hiện cùng nhau, và trong một số trường hợp, việc tra cứu có thể được tái cấu trúc, nhưng đây chỉ là một ví dụ cơ bản.

Ngay cả khi bạn chỉ khởi tạo, bạn vẫn nhận được một hiệu suất:

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

Một lần nữa, YMMV.


5
Trên máy của tôi, cuộc gọi .newInstance () chỉ với một cuộc gọi Class.forName () đạt 30 điểm trở lên. Tùy thuộc vào phiên bản VM, sự khác biệt có thể gần hơn bạn nghĩ với một chiến lược lưu trữ phù hợp.
Sean Reilly

56
@Peter Lawrey dưới đây đã chỉ ra rằng thử nghiệm này hoàn toàn không hợp lệ vì trình biên dịch đã tối ưu hóa giải pháp không phản chiếu (Nó thậm chí có thể chứng minh rằng không có gì được thực hiện và tối ưu hóa vòng lặp for). Cần phải được làm lại và có lẽ nên được loại bỏ khỏi SO là thông tin xấu / sai lệch. Lưu trữ các đối tượng đã tạo trong một mảng trong cả hai trường hợp để ngăn chặn trình tối ưu hóa tối ưu hóa nó. (Nó không thể làm điều này trong tình huống phản chiếu vì nó không thể chứng minh rằng nhà xây dựng không có tác dụng phụ)
Bill K

6
@Bill K - chúng ta đừng mang đi. Có, các số bị tắt do tối ưu hóa. Không, bài kiểm tra không hoàn toàn không hợp lệ. Tôi đã thêm một cuộc gọi loại bỏ bất kỳ khả năng sai lệch kết quả nào và các số vẫn được xếp chồng lên nhau để phản xạ. Trong mọi trường hợp, hãy nhớ rằng đây là một điểm chuẩn vi mô rất thô sơ, điều đó chỉ cho thấy rằng sự phản chiếu luôn phải chịu một chi phí nhất định
Yuval Adam

4
Đây có lẽ là một điểm chuẩn vô dụng. Tùy thuộc vào những gì doS Something làm. Nếu nó không làm gì với hiệu ứng phụ có thể nhìn thấy, thì điểm chuẩn của bạn chỉ chạy mã chết.
nes1983

9
Tôi vừa chứng kiến ​​JVM tối ưu hóa phản xạ 35 lần. Chạy thử nghiệm nhiều lần trong một vòng lặp là cách bạn kiểm tra mã được tối ưu hóa. Lặp lại thứ nhất: 3045ms, lần lặp thứ hai: 2941ms, lần lặp thứ ba: 90ms, lần lặp thứ tư: 83ms. Mã: c.newInstance (i). c là một nhà xây dựng. Mã không phản chiếu: A (i) mới, mang lại 13, 4, 3 .. ms lần. Vì vậy, vâng, phản xạ trong trường hợp này rất chậm, nhưng không chậm hơn nhiều so với những gì mọi người đang kết luận, bởi vì mọi thử nghiệm tôi đang xem, họ chỉ đơn giản là chạy thử nghiệm một lần mà không cho JVM cơ hội thay thế mã byte bằng máy mã.
Mike

87

Vâng, nó chậm hơn.

Nhưng hãy nhớ quy tắc số 1 chết tiệt - TỐI ƯU HÓA TỐI THIỂU LÀ ROOT CỦA TẤT CẢ MỌI THỨ

(Chà, có thể được gắn với # 1 cho DRY)

Tôi thề, nếu ai đó đến gặp tôi tại nơi làm việc và hỏi tôi điều này tôi sẽ rất cảnh giác với mã của họ trong vài tháng tới.

Bạn không bao giờ phải tối ưu hóa cho đến khi bạn chắc chắn rằng bạn cần nó, cho đến lúc đó, chỉ cần viết mã tốt, dễ đọc.

Ồ, và tôi cũng không có nghĩa là viết mã ngu ngốc. Chỉ cần suy nghĩ về cách sạch nhất mà bạn có thể làm - không sao chép và dán, v.v. (Vẫn cảnh giác với những thứ như vòng lặp bên trong và sử dụng bộ sưu tập phù hợp nhất với nhu cầu của bạn - Bỏ qua những chương trình không "tối ưu hóa" này , đó là chương trình "xấu")

Nó làm tôi bối rối khi nghe những câu hỏi như thế này, nhưng sau đó tôi quên rằng mọi người phải tự học tất cả các quy tắc trước khi họ thực sự hiểu. Bạn sẽ nhận được nó sau khi bạn dành một tháng để gỡ lỗi một cái gì đó "Tối ưu hóa".

BIÊN TẬP:

Một điều thú vị đã xảy ra trong chủ đề này. Kiểm tra câu trả lời số 1, đó là một ví dụ về trình biên dịch mạnh mẽ như thế nào trong việc tối ưu hóa mọi thứ. Các thử nghiệm là hoàn toàn không hợp lệ bởi vì việc khởi tạo không phản xạ có thể được hoàn thành.

Bài học? Đừng tối ưu hóa cho đến khi bạn viết một giải pháp được mã hóa gọn gàng và được chứng minh là quá chậm.


28
Tôi hoàn toàn đồng ý với tình cảm của phản hồi này, tuy nhiên nếu bạn chuẩn bị đưa ra một quyết định thiết kế lớn thì sẽ có ý tưởng về hiệu suất để bạn không đi vào một con đường hoàn toàn không khả thi. Có lẽ anh ấy chỉ cần siêng năng?
Hệ thống Limbic

26
-1: Tránh làm mọi thứ sai cách không phải là tối ưu hóa, nó chỉ là làm mọi thứ. Tối ưu hóa đang làm mọi thứ sai cách, phức tạp vì mối quan tâm hiệu suất thực tế hoặc tưởng tượng.
soru

5
@soru hoàn toàn đồng ý. Chọn một danh sách được liên kết trên một danh sách mảng để sắp xếp chèn đơn giản là cách đúng đắn để thực hiện. Nhưng câu hỏi đặc biệt này - có những trường hợp sử dụng tốt cho cả hai mặt của câu hỏi ban đầu, vì vậy việc chọn một câu hỏi dựa trên hiệu suất thay vì giải pháp hữu dụng nhất sẽ là sai. Tôi không chắc chúng tôi không đồng ý chút nào, vì vậy tôi không chắc tại sao bạn lại nói "-1".
Bill K

14
Bất kỳ lập trình viên phân tích hợp lý nào cũng cần xem xét hiệu quả ở giai đoạn đầu hoặc bạn có thể kết thúc với một hệ thống KHÔNG thể được tối ưu hóa trong khung thời gian hiệu quả và tốn kém. Không, bạn không tối ưu hóa mọi chu kỳ đồng hồ nhưng chắc chắn bạn KHÔNG sử dụng các thực tiễn tốt nhất cho những thứ cơ bản như khởi tạo lớp. Ví dụ này là một trong những lý do tuyệt vời TẠI SAO bạn xem xét các câu hỏi như vậy liên quan đến sự phản ánh. Nó sẽ là một lập trình viên khá nghèo, người đi trước và sử dụng sự phản chiếu trong suốt một triệu hệ thống chỉ để sau đó phát hiện ra nó là những đơn đặt hàng có cường độ quá chậm.
RichieHH

2
@Richard Riley Nói chung, khởi tạo lớp là một sự kiện khá hiếm đối với các lớp được chọn mà bạn sẽ sử dụng phản chiếu. Tôi cho rằng bạn đúng mặc dù - một số người có thể khởi tạo mọi lớp một cách phản xạ, ngay cả những người được tái tạo liên tục. Tôi sẽ gọi đó là lập trình khá tệ (mặc dù sau đó COULD triển khai bộ đệm của các thể hiện lớp để sử dụng lại sau thực tế và không làm hại mã của bạn quá nhiều - vì vậy tôi đoán tôi vẫn nói LUÔN thiết kế để dễ đọc, sau đó cấu hình và tối ưu hóa sau)
Bill K

36

Bạn có thể thấy rằng A a = new A () đang được JVM tối ưu hóa. Nếu bạn đặt các đối tượng vào một mảng, chúng sẽ không hoạt động tốt như vậy. ;) Các bản in sau ...

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

Điều này cho thấy sự khác biệt là khoảng 150 ns trên máy của tôi.


Vì vậy, bạn vừa giết trình tối ưu hóa, vì vậy bây giờ cả hai phiên bản đều chậm. Phản xạ là, do đó, vẫn còn chậm.
gbjbaanb

13
@gbjbaanb nếu trình tối ưu hóa tự tối ưu hóa việc tạo thì đó không phải là một thử nghiệm hợp lệ. Do đó, bài kiểm tra của Peter là hợp lệ vì nó thực sự so sánh thời gian tạo (Trình tối ưu hóa sẽ không thể hoạt động trong bất kỳ tình huống nào trong thế giới thực bởi vì trong mọi tình huống trong thế giới thực, bạn cần các đối tượng bạn đang khởi tạo).
Bill K

10
@ nes1983 Trong trường hợp đó bạn có thể đã có cơ hội để tạo điểm chuẩn tốt hơn. Có lẽ bạn có thể cung cấp một cái gì đó mang tính xây dựng, giống như những gì nên có trong cơ thể của phương pháp.
Peter Lawrey

1
trên mac của tôi, openjdk 7u4, sự khác biệt là 95ns so với 100ns. Thay vì lưu trữ A trong mảng, tôi lưu trữ hashCodes. Nếu bạn nói -verbose: class, bạn có thể thấy khi hotspot tạo mã byte để xây dựng A và tăng tốc đi kèm.
Ron

@PeterLawrey Nếu tôi tra cứu một lần (một cuộc gọi đến Class.getDeclaredMethod) và sau đó gọi Method.invokenhiều lần? Tôi có đang sử dụng sự phản chiếu một lần hay nhiều lần khi tôi gọi nó không? Theo dõi câu hỏi, nếu thay vì Methodnó là một Constructorvà tôi làm Constructor.newInstancenhiều lần thì sao?
tmj

28

Nếu thực sự cần một thứ gì đó nhanh hơn sự phản chiếu và nó không chỉ là tối ưu hóa sớm, thì việc tạo mã byte bằng ASM hoặc thư viện cấp cao hơn là một tùy chọn. Việc tạo mã byte lần đầu tiên chậm hơn so với chỉ sử dụng sự phản chiếu, nhưng một khi mã byte được tạo ra, nó sẽ nhanh như mã Java thông thường và sẽ được trình biên dịch JIT tối ưu hóa.

Một số ví dụ về các ứng dụng sử dụng tạo mã:

  • Gọi các phương thức trên các proxy do CGLIB tạo ra nhanh hơn một chút so với các proxy động của Java , bởi vì CGLIB tạo mã byte cho các proxy của nó, nhưng các proxy động chỉ sử dụng phản xạ ( tôi đã đo CGLIB nhanh hơn khoảng 10 lần trong các lệnh gọi phương thức, nhưng việc tạo ra các proxy là chậm hơn).

  • JSerial tạo mã byte để đọc / ghi các trường của các đối tượng được tuần tự hóa, thay vì sử dụng sự phản chiếu. Có một số điểm chuẩn trên trang web của JSerial.

  • Tôi không chắc chắn 100% (và tôi không cảm thấy muốn đọc nguồn ngay bây giờ), nhưng tôi nghĩ Guice tạo mã byte để thực hiện tiêm phụ thuộc. Sửa tôi nếu tôi sai.


27

"Đáng kể" hoàn toàn phụ thuộc vào bối cảnh.

Nếu bạn đang sử dụng sự phản chiếu để tạo một đối tượng xử lý duy nhất dựa trên một số tệp cấu hình và sau đó dành phần còn lại của bạn để chạy các truy vấn cơ sở dữ liệu, thì điều đó không đáng kể. Nếu bạn đang tạo một số lượng lớn các đối tượng thông qua sự phản chiếu trong một vòng lặp chặt chẽ, thì có, điều đó rất có ý nghĩa.

Nói chung, tính linh hoạt trong thiết kế (khi cần thiết!) Sẽ thúc đẩy bạn sử dụng sự phản chiếu, chứ không phải hiệu suất. Tuy nhiên, để xác định xem hiệu suất có phải là vấn đề hay không, bạn cần lập hồ sơ thay vì nhận phản hồi tùy ý từ một diễn đàn thảo luận.


24

Có một số chi phí với sự phản chiếu, nhưng nó nhỏ hơn rất nhiều trên các máy ảo hiện đại so với trước đây.

Nếu bạn đang sử dụng sự phản chiếu để tạo mọi đối tượng đơn giản trong chương trình của mình thì có gì đó không ổn. Thỉnh thoảng sử dụng nó, khi bạn có lý do chính đáng, không nên là một vấn đề.


11

Có, có một điểm nhấn hiệu năng khi sử dụng Reflection nhưng một cách giải quyết khả thi để tối ưu hóa là lưu trữ phương thức:

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

sẽ cho kết quả:

[java] Phương pháp gọi 1000000 lần theo phản xạ với tra cứu mất 5618 triệu

[java] Phương pháp gọi 1000000 lần theo phản xạ với bộ đệm mất 270 mili


Việc sử dụng lại phương thức / hàm tạo thực sự hữu ích và hữu ích, nhưng lưu ý rằng kiểm tra ở trên không đưa ra các con số có ý nghĩa do các vấn đề đo điểm chuẩn thông thường (không có sự khởi động, do đó, vòng lặp đầu tiên chủ yếu là đo thời gian khởi động JVM / JIT).
StaxMan

7

Sự phản chiếu là chậm, mặc dù phân bổ đối tượng không phải là vô vọng như các khía cạnh khác của sự phản ánh. Để đạt được hiệu suất tương đương với khởi tạo dựa trên phản xạ đòi hỏi bạn phải viết mã của mình để jit có thể cho biết lớp nào đang được khởi tạo. Nếu không thể xác định danh tính của lớp, thì mã phân bổ không thể được nội tuyến. Tệ hơn, phân tích thoát thất bại và đối tượng không thể được phân bổ ngăn xếp. Nếu bạn may mắn, cấu hình thời gian chạy của JVM có thể đến cứu nếu mã này bị nóng và có thể xác định động một lớp nào chiếm ưu thế và có thể tối ưu hóa cho lớp đó.

Hãy lưu ý rằng các vi khuẩn trong chủ đề này rất thiếu sót, vì vậy hãy dùng chúng với một hạt muối. Ít sai sót nhất từ ​​trước đến nay là của Peter Lawrey: nó khởi động để có được các phương pháp được đưa ra, và nó (một cách có ý thức) đánh bại phân tích thoát để đảm bảo việc phân bổ đang thực sự xảy ra. Mặc dù vậy, người ta cũng có vấn đề của nó: ví dụ, số lượng lớn các cửa hàng mảng có thể được dự kiến ​​sẽ đánh bại bộ đệm và bộ đệm lưu trữ, do đó, điều này sẽ trở thành chủ yếu là điểm chuẩn bộ nhớ nếu phân bổ của bạn rất nhanh. (Kudos nói với Peter về việc đưa ra kết luận đúng: mặc dù sự khác biệt là "150ns" chứ không phải "2.5x". Tôi nghi ngờ anh ta làm điều này để kiếm sống.)


7

Điều thú vị là, giải quyết setAccessible (đúng), bỏ qua kiểm tra bảo mật, sẽ giảm 20% chi phí.

Không có setAccessible (đúng)

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

Với setAccessible (đúng)

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

1
Có vẻ rõ ràng với tôi về nguyên tắc. Những con số này có quy mô tuyến tính, khi chạy các lệnh 1000000?
Lukas Eder

Trên thực tế setAccessible()có thể có nhiều sự khác biệt hơn nói chung, đặc biệt là đối với các phương thức có nhiều đối số, vì vậy nó phải luôn được gọi.
StaxMan

6

Vâng, nó chậm hơn đáng kể. Chúng tôi đã chạy một số mã đã làm điều đó và hiện tại tôi không có sẵn số liệu, kết quả cuối cùng là chúng tôi phải cấu trúc lại mã đó để không sử dụng phản xạ. Nếu bạn biết lớp này là gì, chỉ cần gọi hàm tạo trực tiếp.


1
+1 Tôi đã có một trải nghiệm tương tự. Thật tốt khi đảm bảo chỉ sử dụng sự phản chiếu nếu thật sự cần thiết.
Ryan Thames

ví dụ các thư viện dựa trên AOP cần sự phản ánh.
gaurav

4

Trong doReflection () là chi phí hoạt động vì Class.forName ("misc.A") (sẽ yêu cầu tra cứu lớp, có khả năng quét đường dẫn lớp trên hệ thống tệp), chứ không phải là newInstance () được gọi trên lớp. Tôi tự hỏi các số liệu thống kê sẽ trông như thế nào nếu Class.forName ("misc.A") chỉ được thực hiện một lần bên ngoài vòng lặp for, nó không thực sự phải được thực hiện cho mỗi lần gọi của vòng lặp.


1

Có, luôn luôn sẽ tạo một đối tượng chậm hơn bằng cách phản chiếu vì JVM không thể tối ưu hóa mã theo thời gian biên dịch. Xem hướng dẫn Phản chiếu của Sun / Java để biết thêm chi tiết.

Xem thử nghiệm đơn giản này:

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
Lưu ý rằng bạn nên tách tra cứu ( Class.forName()) khỏi instanciation (newInstance ()), vì chúng khác nhau đáng kể về đặc tính hiệu suất của chúng và đôi khi bạn có thể tránh việc tra cứu lặp đi lặp lại trong một hệ thống được thiết kế tốt.
Joachim Sauer

3
Ngoài ra: bạn cần thực hiện từng nhiệm vụ nhiều lần để có điểm chuẩn hữu ích: trước hết, các hành động quá chậm để được đo lường một cách đáng tin cậy và thứ hai bạn sẽ cần làm nóng máy ảo HotSpot để có được các số hữu ích.
Joachim Sauer

1

Thường thì bạn có thể sử dụng Apache commons BeanUtils hoặc PropertyUtils mà hướng nội (về cơ bản chúng lưu trữ dữ liệu meta về các lớp để chúng không luôn cần sử dụng sự phản chiếu).


0

Tôi nghĩ rằng nó phụ thuộc vào mức độ nhẹ / nặng của phương pháp mục tiêu. nếu phương thức đích rất nhẹ (ví dụ getter / setter), nó có thể chậm hơn 1 ~ 3 lần. nếu phương thức đích mất khoảng 1 mili giây trở lên, thì hiệu suất sẽ rất gần. đây là thử nghiệm tôi đã làm với Java 8 và Reflasm :

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

Mã kiểm tra hoàn chỉnh có sẵn tại GitHub: ReflectionTest.java

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.