Câu trả lời:
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.
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.
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.
Class.getDeclaredMethod
) và sau đó gọi Method.invoke
nhiề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ì Method
nó là một Constructor
và tôi làm Constructor.newInstance
nhiều lần thì sao?
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.
"Đá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.
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 đề.
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
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.)
Đ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
1000000
?
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.
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.
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.
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");
}
}
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.
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).
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