การสร้างวัตถุโดยใช้การสะท้อนมากกว่าการเรียกตัวสร้างคลาสส่งผลให้เกิดความแตกต่างด้านประสิทธิภาพที่สำคัญหรือไม่?
การสร้างวัตถุโดยใช้การสะท้อนมากกว่าการเรียกตัวสร้างคลาสส่งผลให้เกิดความแตกต่างด้านประสิทธิภาพที่สำคัญหรือไม่?
คำตอบ:
ใช่ - อย่างแน่นอน การมองหาชั้นเรียนผ่านการสะท้อนนั้นมีขนาดใหญ่ขึ้น
การอ้างถึงเอกสารของ Java เกี่ยวกับการสะท้อน :
เนื่องจากการสะท้อนกลับเกี่ยวข้องกับประเภทที่ได้รับการแก้ไขแบบไดนามิกการเพิ่มประสิทธิภาพของเครื่องเสมือน Java บางอย่างจึงไม่สามารถทำได้ ดังนั้นการดำเนินการไตร่ตรองจึงมีประสิทธิภาพการทำงานที่ช้ากว่าคู่ที่ไม่ได้ไตร่ตรองและควรหลีกเลี่ยงในส่วนของโค้ดที่ถูกเรียกใช้บ่อยในแอปพลิเคชันที่ไวต่อประสิทธิภาพ
นี่คือการทดสอบง่ายๆที่ฉันแฮ็คในเครื่องของฉันภายใน 5 นาทีโดยเรียกใช้ 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);
}
}
ด้วยผลลัพธ์เหล่านี้:
35 // no reflection
465 // using reflection
โปรดจำไว้ว่าการค้นหาและการสร้างอินสแตนซ์นั้นทำร่วมกันและในบางกรณีการค้นหาสามารถถูกทำให้ซ้ำได้ แต่นี่เป็นเพียงตัวอย่างพื้นฐาน
แม้ว่าคุณจะยกตัวอย่าง แต่คุณก็ยังได้รับความนิยม:
30 // no reflection
47 // reflection using one lookup, only instantiating
อีกครั้ง YMMV
ใช่มันช้าลง
แต่จำกฎ damn # 1 - การเพิ่มประสิทธิภาพก่อนกำหนดเป็นรากของความชั่วร้ายทั้งหมด
(อาจผูกกับ # 1 สำหรับ DRY)
ฉันสาบานถ้ามีคนมาหาฉันที่ทำงานและถามฉันว่าฉันจะระวังตัวรหัสของพวกเขาในอีกไม่กี่เดือนข้างหน้า
คุณต้องไม่ปรับให้เหมาะสมจนกว่าคุณจะแน่ใจว่าคุณต้องการมันจนกว่าจะถึงตอนนั้นเพียงแค่เขียนโค้ดที่ดีและอ่านได้
โอ้และฉันไม่ได้หมายถึงเขียนโค้ดโง่เช่นกัน แค่คิดเกี่ยวกับวิธีที่สะอาดที่สุดที่คุณสามารถทำได้ - ไม่มีการคัดลอกและวาง ฯลฯ (ยังต้องระวังสิ่งต่าง ๆ เช่นลูปด้านในและการใช้คอลเลกชันที่เหมาะกับความต้องการของคุณมากที่สุด - ไม่สนใจการเขียนโปรแกรมเหล่านี้ มันคือการเขียนโปรแกรม "ไม่ดี")
มันทำให้ฉันประหลาดใจเมื่อฉันได้ยินคำถามเช่นนี้ แต่แล้วฉันก็ลืมว่าทุกคนต้องผ่านการเรียนรู้กฎทั้งหมดก่อนที่จะได้รับ คุณจะได้รับหลังจากที่คุณใช้เวลาหนึ่งเดือนในการดีบั๊กบางสิ่งบางอย่างที่ "เพิ่มประสิทธิภาพ"
แก้ไข:
สิ่งที่น่าสนใจเกิดขึ้นในกระทู้นี้ ตรวจสอบคำตอบ # 1 มันเป็นตัวอย่างของคอมไพเลอร์ที่มีประสิทธิภาพในการเพิ่มประสิทธิภาพของสิ่งต่าง ๆ การทดสอบไม่ถูกต้องสมบูรณ์เนื่องจากการแยกอินสแตนซ์ที่ไม่ใช่ไตร่ตรองนั้นสามารถนำมาพิจารณาได้อย่างสมบูรณ์
บทเรียน? อย่าปรับให้เหมาะสมจนกว่าคุณจะเขียนโซลูชันที่สะอาดและมีการเข้ารหัสอย่างเรียบร้อยและพิสูจน์แล้วว่าช้าเกินไป
คุณอาจพบว่า 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 บนเครื่องของฉัน
Class.getDeclaredMethod
) แล้วโทรMethod.invoke
หลาย ๆ ครั้ง? ฉันใช้การสะท้อนหนึ่งครั้งหรือหลายครั้งตามที่ฉันเรียกใช้หรือไม่ คำถามติดตามจะเกิดอะไรขึ้นถ้าแทนที่จะMethod
เป็นConstructor
และฉันทำConstructor.newInstance
หลาย ๆ ครั้ง?
หากมีความต้องการสิ่งที่เร็วกว่าการสะท้อนจริงๆและไม่ใช่เพียงการปรับให้เหมาะสมก่อนกำหนดดังนั้นการสร้าง bytecode ด้วยASMหรือไลบรารีระดับสูงขึ้นเป็นตัวเลือก การสร้าง bytecode ในครั้งแรกช้ากว่าการใช้การสะท้อนกลับ แต่เมื่อ bytecode ถูกสร้างขึ้นมันจะเร็วพอ ๆ กับโค้ด Java ปกติและจะถูกปรับให้เหมาะสมโดยคอมไพเลอร์ JIT
ตัวอย่างแอปพลิเคชันที่ใช้การสร้างรหัส:
วิธีการเรียกใช้บนพร็อกซีที่สร้างโดยCGLIBนั้นเร็วกว่าพร็อกซีแบบไดนามิกของ Java เล็กน้อยเนื่องจาก CGLIB สร้าง bytecode สำหรับพร็อกซีของตน แต่พร็อกซีแบบไดนามิกใช้การสะท้อนเท่านั้น ( ฉันวัด CGLIB
JSerialสร้าง bytecode สำหรับการอ่าน / เขียนฟิลด์ของวัตถุที่เป็นอนุกรมแทนที่จะใช้การสะท้อน มีมาตรฐานบางอย่างในเว็บไซต์ของ JSerial
ฉันไม่แน่ใจ 100% (และฉันไม่รู้สึกอยากอ่านที่มาตอนนี้) แต่ฉันคิดว่าGuiceสร้าง bytecode เพื่อทำการฉีดพึ่งพา ช่วยแก้ให้ด้วยนะถ้าฉันผิด.
"สำคัญ" ขึ้นอยู่กับบริบททั้งหมด
หากคุณกำลังใช้การสะท้อนเพื่อสร้างวัตถุตัวจัดการเดียวโดยยึดตามแฟ้มการกำหนดค่าบางอย่างจากนั้นใช้เวลาที่เหลือในการรันการสืบค้นฐานข้อมูล หากคุณกำลังสร้างวัตถุจำนวนมากผ่านการสะท้อนในวงแคบ ๆ แล้วใช่มันมีความสำคัญ
โดยทั่วไปความยืดหยุ่นในการออกแบบ (ที่จำเป็น!) ควรผลักดันการใช้งานการสะท้อนของคุณไม่ใช่ประสิทธิภาพ อย่างไรก็ตามเพื่อตรวจสอบว่าประสิทธิภาพเป็นปัญหาหรือไม่คุณต้องทำโปรไฟล์แทนรับการตอบสนองโดยพลการจากฟอรัมการสนทนา
มีค่าใช้จ่ายที่มีการสะท้อน แต่มันมีขนาดเล็กกว่าบน VMs ที่ทันสมัยกว่าที่เคยเป็น
หากคุณใช้การไตร่ตรองเพื่อสร้างทุกวัตถุอย่างง่ายในโปรแกรม ใช้เป็นครั้งคราวเมื่อคุณมีเหตุผลที่ดีไม่ควรเป็นปัญหาเลย
ใช่มีประสิทธิภาพในการทำงานเมื่อใช้ 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 ครั้งด้วยการค้นหาอย่างละเอียดใช้เวลา 5,618 มิลลิวินาที
[java] วิธีการโทร 1000000 ครั้งโดยใช้แคชแคช 270 มิลลิวินาที
การสะท้อนกลับช้าแม้ว่าการจัดสรรวัตถุจะไม่สิ้นหวังเหมือนการสะท้อนในด้านอื่น ๆ เพื่อให้ได้ประสิทธิภาพที่เทียบเท่ากับการสร้างอินสแตนซ์สะท้อนแสงคุณต้องเขียนโค้ดของคุณเพื่อให้ jit สามารถบอกได้ว่าคลาสใดถูก instantiate หากไม่สามารถระบุข้อมูลประจำตัวของคลาสได้รหัสการจัดสรรจะไม่สามารถถูกขีดเส้นใต้ได้ ที่แย่กว่านั้นการวิเคราะห์การหลบหนีล้มเหลวและไม่สามารถจัดสรรวัตถุได้ หากคุณโชคดีการทำโปรไฟล์แบบรันไทม์ของ JVM อาจมาช่วยหากรหัสนี้ร้อนและอาจกำหนดว่าคลาสใดที่มีอิทธิพลเหนือคลาสและอาจปรับให้เหมาะสมสำหรับคลาสนั้น
ระวัง microbenchmarks ในหัวข้อนี้มีข้อบกพร่องอย่างลึกดังนั้นใช้พวกเขาด้วยเม็ดเกลือ ข้อบกพร่องเล็ก ๆ น้อย ๆ ของปีเตอร์ลอว์เรย์คือ: มันอุ่นเครื่องเพื่อให้ได้วิธีการที่เหมาะสมและมัน (มีสติ) เอาชนะการวิเคราะห์ทางหนีเพื่อให้แน่ใจว่าการจัดสรรจะเกิดขึ้นจริง แม้ว่าจะมีปัญหาก็ตามตัวอย่างเช่นจำนวนร้านค้าอาร์เรย์จำนวนมากสามารถคาดหวังว่าจะเอาชนะแคชและบัฟเฟอร์ร้านค้าดังนั้นสิ่งนี้จะกลายเป็นมาตรฐานหน่วยความจำส่วนใหญ่หากการจัดสรรของคุณเร็วมาก (ความรุ่งโรจน์ถึง Peter ในการได้ข้อสรุปว่า: ความแตกต่างคือ "150ns" มากกว่า "2.5x" ฉันสงสัยว่าเขาทำสิ่งนี้เพื่อหาเลี้ยงชีพ)
ที่น่าสนใจคือการตั้งค่า setAccessible (true) ซึ่งข้ามการตรวจสอบความปลอดภัยมีค่าใช้จ่ายลดลง 20%
ไม่มี setAccessible (จริง)
new A(), 70 ns
A.class.newInstance(), 214 ns
new A(), 84 ns
A.class.newInstance(), 229 ns
ด้วย setAccessible (จริง)
new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns
1000000
การร้องขอหรือไม่
setAccessible()
จะมีความแตกต่างได้มากขึ้นโดยเฉพาะอย่างยิ่งสำหรับวิธีการที่มีอาร์กิวเมนต์หลายตัวดังนั้นจึงควรเรียกใช้เสมอ
ใช่มันช้าลงอย่างมาก เรากำลังเรียกใช้โค้ดบางอย่างที่ทำเช่นนั้นและในขณะที่ฉันไม่มีเมทริกให้ใช้งานในตอนนี้ผลลัพธ์ที่ได้คือเราต้องปรับโครงสร้างโค้ดนั้นใหม่เพื่อไม่ให้ใช้การสะท้อน ถ้าคุณรู้ว่าคลาสคืออะไรให้เรียกนวกรรมิกโดยตรง
ใน doReflection () เป็นค่าใช้จ่ายเนื่องจาก Class.forName ("misc.A") (ที่จะต้องมีการค้นหาชั้นเรียนอาจสแกนเส้นทางคลาสบนระบบไฟล์) แทนที่จะเรียก newInstance () บนคลาส ฉันสงสัยว่าสถิติจะเป็นอย่างไรถ้า Class.forName ("misc.A") ทำเพียงครั้งเดียวนอกวง for-loop มันไม่จำเป็นต้องทำทุกครั้งที่มีการวนซ้ำ
ใช่จะทำให้การสร้างวัตถุช้าลงโดยการไตร่ตรองเพราะ 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");
}
}
Class.forName()
) ออกจาก instanciation (newInstance ()) เนื่องจากมีความแตกต่างอย่างมากในลักษณะการทำงานและคุณสามารถหลีกเลี่ยงการค้นหาซ้ำ ๆ ในระบบที่ออกแบบมาอย่างดี
บ่อยครั้งที่คุณสามารถใช้ Apache คอมมอนส์ BeanUtils หรือ PropertyUtils ซึ่งวิปัสสนา (โดยทั่วไปแล้วพวกเขาเก็บข้อมูลเมตาเกี่ยวกับชั้นเรียนเพื่อให้พวกเขาไม่จำเป็นต้องใช้การสะท้อน)
ฉันคิดว่ามันขึ้นอยู่กับว่าเป้าหมายนั้นเบาหรือหนักแค่ไหน ถ้าวิธีเป้าหมายเบามาก (เช่น getter / setter) มันอาจช้ากว่า 1 ~ 3 เท่า หากวิธีเป้าหมายใช้เวลาประมาณ 1 มิลลิวินาทีหรือมากกว่านั้นประสิทธิภาพจะใกล้เคียงกันมาก นี่คือการทดสอบที่ฉันทำกับ Java 8 และการสะท้อนกลับ :
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