บทความนี้จะเป็นบทความที่สองในชุดการเพิ่มประสิทธิภาพการทำงานของ JVM (บทความแรก: พอร์ทัล) และคอมไพลเลอร์ Java จะเป็นเนื้อหาหลักที่กล่าวถึงในบทความนี้
ในบทความนี้ ผู้เขียน (Eva Andreasson) จะแนะนำคอมไพเลอร์ประเภทต่างๆ ก่อน และเปรียบเทียบประสิทธิภาพการทำงานของการคอมไพล์ฝั่งไคลเอ็นต์ คอมไพเลอร์ฝั่งเซิร์ฟเวอร์ และการคอมไพล์หลายเลเยอร์ จากนั้น ในตอนท้ายของบทความ เราจะแนะนำวิธีการเพิ่มประสิทธิภาพ JVM ทั่วไปหลายวิธี เช่น การกำจัดโค้ดที่ไม่ทำงาน การฝังโค้ด และการปรับแต่งเนื้อหาลูปให้เหมาะสม
คุณลักษณะที่น่าภาคภูมิใจที่สุดของ Java คือ "ความเป็นอิสระของแพลตฟอร์ม" มีต้นกำเนิดมาจากคอมไพเลอร์ Java นักพัฒนาซอฟต์แวร์พยายามอย่างดีที่สุดเพื่อเขียนแอปพลิเคชัน Java ที่ดีที่สุดเท่าที่จะเป็นไปได้ และคอมไพเลอร์ทำงานเบื้องหลังเพื่อสร้างโค้ดปฏิบัติการที่มีประสิทธิภาพตามแพลตฟอร์มเป้าหมาย คอมไพเลอร์ที่แตกต่างกันมีความเหมาะสมสำหรับข้อกำหนดการใช้งานที่แตกต่างกัน ดังนั้นจึงให้ผลลัพธ์การปรับให้เหมาะสมที่แตกต่างกัน ดังนั้น หากคุณสามารถเข้าใจวิธีการทำงานของคอมไพเลอร์ได้ดีขึ้น และรู้จักคอมไพเลอร์ประเภทต่างๆ มากขึ้น คุณก็สามารถปรับโปรแกรม Java ของคุณให้เหมาะสมได้ดีขึ้น
บทความนี้เน้นและอธิบายความแตกต่างระหว่างคอมไพเลอร์เครื่องเสมือน Java ต่างๆ ในเวลาเดียวกัน ฉันจะพูดถึงโซลูชันการปรับให้เหมาะสมบางอย่างที่คอมไพเลอร์แบบทันเวลา (JIT) ใช้กันทั่วไป
คอมไพเลอร์คืออะไร?
พูดง่ายๆ ก็คือ คอมไพลเลอร์ใช้โปรแกรมภาษาการเขียนโปรแกรมเป็นอินพุต และโปรแกรมภาษาที่ปฏิบัติการได้อื่นเป็นเอาต์พุต Javac เป็นคอมไพเลอร์ที่พบบ่อยที่สุด มันมีอยู่ใน JDK ทั้งหมด Javac รับโค้ด Java เป็นเอาต์พุตและแปลงเป็นโค้ดปฏิบัติการ JVM - bytecode รหัสไบต์เหล่านี้ถูกจัดเก็บไว้ในไฟล์ที่ลงท้ายด้วย .class และโหลดเข้าสู่สภาพแวดล้อมรันไทม์ของ Java เมื่อโปรแกรม Java เริ่มทำงาน
CPU ไม่สามารถอ่าน bytecode ได้โดยตรง นอกจากนี้ยังจำเป็นต้องแปลเป็นภาษาคำสั่งของเครื่องที่แพลตฟอร์มปัจจุบันสามารถเข้าใจได้ มีคอมไพเลอร์อีกตัวหนึ่งใน JVM ที่รับผิดชอบในการแปลรหัสไบต์เป็นคำสั่งที่ปฏิบัติการได้โดยแพลตฟอร์มเป้าหมาย คอมไพเลอร์ JVM บางตัวต้องการขั้นตอนโค้ดไบต์หลายระดับ ตัวอย่างเช่น คอมไพลเลอร์อาจต้องผ่านขั้นตอนกลางหลายรูปแบบก่อนที่จะแปลไบต์โค้ดเป็นคำสั่งของเครื่อง
จากมุมมองของผู้ไม่เชื่อเรื่องพระเจ้าของแพลตฟอร์ม เราต้องการให้โค้ดของเราไม่เชื่อเรื่องพระเจ้าของแพลตฟอร์มมากที่สุดเท่าที่จะเป็นไปได้
เพื่อให้บรรลุเป้าหมายนี้ เราทำงานในระดับสุดท้ายของการแปล ตั้งแต่การแสดงโค้ดไบต์ต่ำสุดไปจนถึงโค้ดเครื่องจริง ซึ่งเชื่อมโยงโค้ดที่ปฏิบัติการได้เข้ากับสถาปัตยกรรมของแพลตฟอร์มเฉพาะอย่างแท้จริง จากระดับสูงสุด เราสามารถแบ่งคอมไพเลอร์ออกเป็นคอมไพเลอร์แบบคงที่และคอมไพเลอร์แบบไดนามิกได้ เราสามารถเลือกคอมไพเลอร์ที่เหมาะสมตามสภาพแวดล้อมการดำเนินการเป้าหมาย ผลลัพธ์การปรับให้เหมาะสมที่เราต้องการ และข้อจำกัดด้านทรัพยากรที่เราจำเป็นต้องปฏิบัติตาม ในบทความก่อนหน้านี้ เราได้พูดคุยกันสั้นๆ เกี่ยวกับสแตติกคอมไพเลอร์และไดนามิกคอมไพเลอร์ และในส่วนต่อไปนี้ เราจะอธิบายให้ละเอียดยิ่งขึ้น
การคอมไพล์แบบคงที่ VS การคอมไพล์แบบไดนามิก
javac ที่เรากล่าวถึงก่อนหน้านี้เป็นตัวอย่างของการคอมไพล์แบบคงที่ ด้วยคอมไพลเลอร์แบบคงที่ โค้ดอินพุตจะถูกตีความหนึ่งครั้ง และเอาต์พุตคือรูปแบบที่โปรแกรมจะถูกดำเนินการในอนาคต เว้นแต่คุณจะอัปเดตซอร์สโค้ดและคอมไพล์ใหม่ (ผ่านคอมไพเลอร์) ผลลัพธ์การทำงานของโปรแกรมจะไม่เปลี่ยนแปลง เนื่องจากอินพุตเป็นอินพุตแบบคงที่ และคอมไพเลอร์เป็นคอมไพเลอร์แบบคงที่
ด้วยการคอมไพล์แบบคงที่ โปรแกรมต่อไปนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
staticint add7 (int x ) { กลับ x + 7;}
จะถูกแปลงเป็น bytecode คล้ายกับตัวอย่างต่อไปนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
iload0 bipush 7 iadd ireturn
คอมไพเลอร์แบบไดนามิกจะคอมไพล์ภาษาหนึ่งเป็นภาษาอื่นแบบไดนามิก ที่เรียกว่าไดนามิกหมายถึงการคอมไพล์ในขณะที่โปรแกรมกำลังทำงาน - คอมไพล์ขณะทำงาน! ข้อดีของการคอมไพล์และการเพิ่มประสิทธิภาพแบบไดนามิกคือสามารถจัดการกับการเปลี่ยนแปลงบางอย่างได้เมื่อโหลดแอปพลิเคชัน รันไทม์ของ Java มักจะทำงานในสภาพแวดล้อมที่คาดเดาไม่ได้หรือแม้กระทั่งการเปลี่ยนแปลง ดังนั้นการคอมไพล์แบบไดนามิกจึงเหมาะมากสำหรับรันไทม์ของ Java JVM ส่วนใหญ่ใช้คอมไพเลอร์แบบไดนามิก เช่น คอมไพเลอร์ JIT เป็นที่น่าสังเกตว่าการคอมไพล์แบบไดนามิกและการเพิ่มประสิทธิภาพโค้ดจำเป็นต้องใช้โครงสร้างข้อมูลเพิ่มเติม เธรด และทรัพยากร CPU เพิ่มเติม ยิ่งเครื่องมือเพิ่มประสิทธิภาพหรือตัววิเคราะห์บริบทโค้ดไบต์ขั้นสูงมากเท่าไรก็ยิ่งใช้ทรัพยากรมากขึ้นเท่านั้น แต่ค่าใช้จ่ายเหล่านี้มีน้อยมากเมื่อเทียบกับการปรับปรุงประสิทธิภาพที่สำคัญ
ประเภท JVM และความเป็นอิสระของแพลตฟอร์มของ Java
คุณสมบัติทั่วไปของการใช้งาน JVM ทั้งหมดคือการคอมไพล์ bytecode ลงในคำสั่งเครื่อง JVM บางตัวตีความโค้ดเมื่อโหลดแอปพลิเคชันและใช้ตัวนับประสิทธิภาพเพื่อค้นหาโค้ด "ร้อน" ส่วนบางตัวทำสิ่งนี้ผ่านการคอมไพล์ ปัญหาหลักของการคอมไพล์คือการรวมศูนย์ต้องใช้ทรัพยากรจำนวนมาก แต่ยังนำไปสู่การเพิ่มประสิทธิภาพที่ดีขึ้นอีกด้วย
หากคุณยังใหม่กับ Java ความซับซ้อนของ JVM จะทำให้คุณสับสนอย่างแน่นอน แต่ข่าวดีก็คือคุณไม่จำเป็นต้องคิดออก! JVM จะจัดการการคอมไพล์และการเพิ่มประสิทธิภาพโค้ด และคุณไม่จำเป็นต้องกังวลเกี่ยวกับคำสั่งเครื่องและวิธีการเขียนโค้ดให้ตรงกับสถาปัตยกรรมของแพลตฟอร์มที่โปรแกรมกำลังทำงานอยู่มากที่สุด
จาก java bytecode ไปจนถึงปฏิบัติการได้
เมื่อโค้ด Java ของคุณถูกคอมไพล์เป็น bytecode แล้ว ขั้นตอนต่อไปคือการแปลคำสั่ง bytecode เป็นโค้ดเครื่อง ขั้นตอนนี้สามารถนำไปใช้ผ่านล่ามหรือผ่านคอมไพเลอร์
อธิบาย
การตีความเป็นวิธีที่ง่ายที่สุดในการรวบรวมไบต์โค้ด ล่ามจะค้นหาคำสั่งฮาร์ดแวร์ที่สอดคล้องกับคำสั่ง bytecode แต่ละคำสั่งในรูปแบบของตารางค้นหา จากนั้นจะส่งคำสั่งนั้นไปยัง CPU เพื่อดำเนินการ
คุณสามารถนึกถึงล่ามเหมือนพจนานุกรม: สำหรับแต่ละคำเฉพาะ (คำสั่ง bytecode) จะมีคำแปลเฉพาะ (คำสั่งรหัสเครื่อง) ที่สอดคล้องกัน เนื่องจากล่ามดำเนินการคำสั่งทันทีทุกครั้งที่อ่าน วิธีการนี้จึงไม่สามารถปรับชุดคำสั่งให้เหมาะสมได้ ในเวลาเดียวกัน ทุกครั้งที่มีการเรียก bytecode จะต้องถูกตีความทันที ดังนั้นล่ามจึงทำงานช้ามาก ล่ามรันโค้ดในลักษณะที่แม่นยำมาก แต่เนื่องจากชุดคำสั่งเอาต์พุตไม่ได้รับการปรับให้เหมาะสม จึงอาจไม่ให้ผลลัพธ์ที่ดีที่สุดสำหรับโปรเซสเซอร์ของแพลตฟอร์มเป้าหมาย
รวบรวม
คอมไพเลอร์จะโหลดโค้ดทั้งหมดเพื่อเรียกใช้รันไทม์ วิธีนี้สามารถอ้างถึงบริบทรันไทม์ทั้งหมดหรือบางส่วนเมื่อแปลโค้ดไบต์ การตัดสินใจจะขึ้นอยู่กับผลลัพธ์ของการวิเคราะห์กราฟโค้ด เช่นการเปรียบเทียบสาขาการดำเนินการที่แตกต่างกันและการอ้างอิงข้อมูลบริบทรันไทม์
หลังจากแปลลำดับไบต์โค้ดเป็นชุดคำสั่งรหัสเครื่องแล้ว การปรับให้เหมาะสมที่สุดสามารถดำเนินการได้ตามชุดคำสั่งรหัสเครื่องนี้ ชุดคำสั่งที่ได้รับการปรับปรุงจะถูกจัดเก็บไว้ในโครงสร้างที่เรียกว่าโค้ดบัฟเฟอร์ เมื่อโค้ดไบต์เหล่านี้ถูกดำเนินการอีกครั้ง โค้ดที่ได้รับการปรับปรุงแล้วสามารถรับได้โดยตรงจากบัฟเฟอร์โค้ดนี้และดำเนินการ ในบางกรณี คอมไพลเลอร์ไม่ได้ใช้เครื่องมือเพิ่มประสิทธิภาพเพื่อปรับโค้ดให้เหมาะสม แต่ใช้ลำดับการปรับให้เหมาะสมใหม่ - "การนับประสิทธิภาพ"
ข้อดีของการใช้โค้ดแคชคือชุดคำสั่งผลลัพธ์สามารถดำเนินการได้ทันทีโดยไม่จำเป็นต้องตีความใหม่หรือคอมไพล์!
วิธีนี้จะช่วยลดเวลาในการดำเนินการได้อย่างมาก โดยเฉพาะอย่างยิ่งสำหรับแอปพลิเคชัน Java ที่มีการเรียกใช้เมธอดหลายครั้ง
การเพิ่มประสิทธิภาพ
ด้วยการแนะนำการคอมไพล์แบบไดนามิก เรามีโอกาสที่จะแทรกตัวนับประสิทธิภาพ ตัวอย่างเช่น คอมไพเลอร์แทรกตัวนับประสิทธิภาพที่เพิ่มขึ้นทุกครั้งที่มีการเรียกบล็อกของไบต์โค้ด (ที่สอดคล้องกับวิธีการเฉพาะ) คอมไพลเลอร์ใช้ตัวนับเหล่านี้เพื่อค้นหา "บล็อกลัด" เพื่อให้สามารถกำหนดได้ว่าบล็อกโค้ดใดที่สามารถปรับให้เหมาะสมเพื่อนำการปรับปรุงประสิทธิภาพสูงสุดมาสู่แอปพลิเคชัน ข้อมูลการวิเคราะห์ประสิทธิภาพรันไทม์สามารถช่วยให้คอมไพลเลอร์ตัดสินใจเพิ่มประสิทธิภาพได้มากขึ้นในสถานะออนไลน์ ซึ่งจะช่วยปรับปรุงประสิทธิภาพการเรียกใช้โค้ดให้ดียิ่งขึ้น เนื่องจากเราได้รับข้อมูลการวิเคราะห์ประสิทธิภาพของโค้ดที่แม่นยำมากขึ้นเรื่อยๆ เราจึงสามารถค้นหาจุดเพิ่มประสิทธิภาพได้มากขึ้นและตัดสินใจเพิ่มประสิทธิภาพได้ดีขึ้น เช่น วิธีจัดลำดับคำสั่งให้ดีขึ้น และจะใช้ชุดคำสั่งที่มีประสิทธิภาพมากขึ้นหรือไม่ และ ไม่ว่าจะกำจัดการดำเนินงานที่ซ้ำซ้อน ฯลฯ
ตัวอย่างเช่น
พิจารณาโค้ด Java ต่อไปนี้ คัดลอกโค้ด รหัสจะเป็นดังนี้:
staticint add7 (int x ) { กลับ x + 7;}
Javac จะแปลแบบคงที่เป็นโค้ดไบต์ต่อไปนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
ไอโหลด0
ไบพุช 7
ไอแอด
กลับคืนมา
เมื่อเรียกใช้เมธอดนี้ bytecode จะถูกคอมไพล์เป็นคำสั่งเครื่องแบบไดนามิก วิธีการนี้อาจได้รับการปรับให้เหมาะสมเมื่อตัวนับประสิทธิภาพ (ถ้ามี) ถึงเกณฑ์ที่กำหนด ผลลัพธ์ที่ได้รับการปรับปรุงอาจมีลักษณะเหมือนกับชุดคำสั่งเครื่องต่อไปนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
lea rax,[rdx+7] ret
คอมไพเลอร์ที่แตกต่างกันเหมาะสำหรับการใช้งานที่แตกต่างกัน
การใช้งานที่แตกต่างกันมีความต้องการที่แตกต่างกัน แอปพลิเคชันฝั่งเซิร์ฟเวอร์ระดับองค์กรมักจะต้องทำงานเป็นเวลานาน ดังนั้นพวกเขามักจะต้องการการปรับประสิทธิภาพให้เหมาะสมมากขึ้น ในขณะที่แอปเพล็ตฝั่งไคลเอ็นต์อาจต้องการเวลาตอบสนองที่เร็วขึ้นและใช้ทรัพยากรน้อยลง เรามาหารือเกี่ยวกับคอมไพเลอร์สามตัวที่แตกต่างกันพร้อมข้อดีและข้อเสีย
คอมไพเลอร์ฝั่งไคลเอ็นต์
C1 เป็นคอมไพเลอร์เพิ่มประสิทธิภาพที่รู้จักกันดี เมื่อเริ่มต้น JVM ให้เพิ่มพารามิเตอร์ -client เพื่อเริ่มต้นคอมไพเลอร์ จากชื่อของมัน เราจะพบว่า C1 เป็นไคลเอนต์คอมไพเลอร์ เหมาะอย่างยิ่งสำหรับแอปพลิเคชันไคลเอนต์ที่มีทรัพยากรระบบน้อยหรือต้องการการเริ่มต้นระบบที่รวดเร็ว C1 ทำการเพิ่มประสิทธิภาพโค้ดโดยใช้ตัวนับประสิทธิภาพ นี่เป็นวิธีการเพิ่มประสิทธิภาพแบบง่ายๆ โดยมีการแทรกแซงซอร์สโค้ดน้อยลง
คอมไพเลอร์ฝั่งเซิร์ฟเวอร์
สำหรับแอปพลิเคชันที่รันระยะยาว (เช่นแอปพลิเคชันระดับองค์กรฝั่งเซิร์ฟเวอร์) การใช้คอมไพเลอร์ฝั่งไคลเอ็นต์อาจไม่เพียงพอ ในเวลานี้เราควรเลือกคอมไพเลอร์ฝั่งเซิร์ฟเวอร์เช่น C2 เครื่องมือเพิ่มประสิทธิภาพสามารถเริ่มต้นได้โดยการเพิ่มเซิร์ฟเวอร์ในบรรทัดเริ่มต้น JVM เนื่องจากโดยทั่วไปแอปพลิเคชันฝั่งเซิร์ฟเวอร์ส่วนใหญ่จะรันนาน คุณจะสามารถรวบรวมข้อมูลการปรับประสิทธิภาพให้เหมาะสมได้มากขึ้นโดยใช้คอมไพลเลอร์ C2 มากกว่าแอปพลิเคชันฝั่งไคลเอ็นต์ที่ทำงานระยะสั้นและมีน้ำหนักเบา ดังนั้นคุณจะสามารถใช้เทคนิคและอัลกอริธึมการเพิ่มประสิทธิภาพขั้นสูงเพิ่มเติมได้
เคล็ดลับ: อุ่นเครื่องคอมไพเลอร์ฝั่งเซิร์ฟเวอร์ของคุณ
สำหรับการปรับใช้ฝั่งเซิร์ฟเวอร์ คอมไพเลอร์อาจใช้เวลาสักครู่เพื่อปรับโค้ด "hot" เหล่านั้นให้เหมาะสม ดังนั้นการปรับใช้ฝั่งเซิร์ฟเวอร์จึงมักต้องมีระยะ "อุ่นเครื่อง" ดังนั้นเมื่อดำเนินการวัดประสิทธิภาพในการใช้งานฝั่งเซิร์ฟเวอร์ ควรตรวจสอบให้แน่ใจเสมอว่าแอปพลิเคชันของคุณอยู่ในสถานะคงที่! การให้เวลาคอมไพเลอร์เพียงพอในการคอมไพล์จะเป็นประโยชน์มากมายต่อแอปพลิเคชันของคุณ
คอมไพเลอร์ฝั่งเซิร์ฟเวอร์สามารถรับข้อมูลการปรับแต่งประสิทธิภาพได้มากกว่าคอมไพเลอร์ฝั่งไคลเอ็นต์ เพื่อให้สามารถดำเนินการวิเคราะห์สาขาที่ซับซ้อนมากขึ้น และค้นหาเส้นทางการปรับให้เหมาะสมด้วยประสิทธิภาพที่ดีกว่า ยิ่งคุณมีข้อมูลการวิเคราะห์ประสิทธิภาพมากเท่าใด ผลการวิเคราะห์แอปพลิเคชันของคุณก็จะยิ่งดีขึ้นเท่านั้น แน่นอนว่า การวิเคราะห์ประสิทธิภาพอย่างกว้างขวางต้องใช้ทรัพยากรคอมไพเลอร์มากขึ้น ตัวอย่างเช่น หาก JVM ใช้คอมไพลเลอร์ C2 ก็จำเป็นต้องใช้รอบของ CPU มากขึ้น โค้ดแคชที่ใหญ่ขึ้น เป็นต้น
การรวบรวมหลายระดับ
การคอมไพล์หลายระดับเป็นการผสมผสานการคอมไพล์ฝั่งไคลเอ็นต์และการคอมไพล์ฝั่งเซิร์ฟเวอร์ Azul เป็นคนแรกที่ใช้การคอมไพล์หลายเลเยอร์ใน Zing JVM ของเขา ล่าสุด เทคโนโลยีนี้ถูกนำมาใช้โดย Oracle Java Hotspot JVM (หลัง Java SE7) การคอมไพล์หลายระดับรวมข้อดีของคอมไพเลอร์ฝั่งไคลเอ็นต์และฝั่งเซิร์ฟเวอร์เข้าด้วยกัน คอมไพเลอร์ไคลเอนต์ทำงานในสองสถานการณ์: เมื่อแอปพลิเคชันเริ่มทำงาน และเมื่อตัวนับประสิทธิภาพถึงเกณฑ์ระดับล่างเพื่อดำเนินการปรับประสิทธิภาพให้เหมาะสม คอมไพเลอร์ไคลเอนต์ยังแทรกตัวนับประสิทธิภาพและเตรียมชุดคำสั่งสำหรับใช้ในภายหลังโดยคอมไพเลอร์ฝั่งเซิร์ฟเวอร์เพื่อการเพิ่มประสิทธิภาพขั้นสูง การคอมไพล์หลายชั้นเป็นวิธีการวิเคราะห์ประสิทธิภาพที่มีการใช้ทรัพยากรสูง เนื่องจากรวบรวมข้อมูลระหว่างกิจกรรมคอมไพเลอร์ที่มีผลกระทบต่ำ ข้อมูลนี้จึงสามารถนำมาใช้ในภายหลังในการปรับแต่งขั้นสูงเพิ่มเติมได้ วิธีการนี้ให้ข้อมูลมากกว่าการวิเคราะห์ตัวนับโดยใช้โค้ดที่สื่อความหมาย
รูปที่ 1 อธิบายการเปรียบเทียบประสิทธิภาพของล่าม การคอมไพล์ฝั่งไคลเอ็นต์ การคอมไพล์ฝั่งเซิร์ฟเวอร์ และการคอมไพล์หลายเลเยอร์ แกน X คือเวลาดำเนินการ (หน่วยของเวลา) และแกน Y คือประสิทธิภาพ (จำนวนการดำเนินการต่อหน่วยเวลา)
รูปที่ 1. การเปรียบเทียบประสิทธิภาพของคอมไพเลอร์
เมื่อเทียบกับโค้ดที่ตีความเพียงอย่างเดียว การใช้คอมไพลเลอร์ฝั่งไคลเอ็นต์สามารถนำมาซึ่งการปรับปรุงประสิทธิภาพได้ประมาณ 5 ถึง 10 เท่า จำนวนประสิทธิภาพที่เพิ่มขึ้นที่คุณได้รับนั้นขึ้นอยู่กับประสิทธิภาพของคอมไพลเลอร์ ประเภทของเครื่องมือเพิ่มประสิทธิภาพที่มี และการออกแบบแอปพลิเคชันที่ตรงกับแพลตฟอร์มเป้าหมายได้ดีเพียงใด แต่สำหรับนักพัฒนาโปรแกรม สิ่งสุดท้ายมักจะถูกมองข้ามไป
เมื่อเปรียบเทียบกับคอมไพเลอร์ฝั่งไคลเอ็นต์ คอมไพเลอร์ฝั่งเซิร์ฟเวอร์มักจะได้รับการปรับปรุงประสิทธิภาพ 30% ถึง 50% ในกรณีส่วนใหญ่ การปรับปรุงประสิทธิภาพมักจะมาพร้อมกับต้นทุนการใช้ทรัพยากร
การคอมไพล์หลายระดับรวมข้อดีของคอมไพเลอร์ทั้งสองเข้าด้วยกัน การคอมไพล์ฝั่งไคลเอ็นต์มีเวลาเริ่มต้นที่สั้นกว่าและสามารถดำเนินการปรับให้เหมาะสมได้อย่างรวดเร็ว การคอมไพล์ฝั่งเซิร์ฟเวอร์สามารถดำเนินการปรับให้เหมาะสมขั้นสูงยิ่งขึ้นในระหว่างกระบวนการดำเนินการที่ตามมา
การเพิ่มประสิทธิภาพคอมไพเลอร์ทั่วไปบางอย่าง
จนถึงตอนนี้ เราได้พูดคุยถึงความหมายของการเพิ่มประสิทธิภาพโค้ดแล้ว และอย่างไรและเมื่อใดที่ JVM ดำเนินการเพิ่มประสิทธิภาพโค้ด ต่อไป ฉันจะจบบทความนี้โดยแนะนำวิธีการเพิ่มประสิทธิภาพบางอย่างที่คอมไพเลอร์ใช้จริง การปรับให้เหมาะสม JVM จริง ๆ แล้วเกิดขึ้นที่ระยะไบต์โค้ด (หรือระยะการแสดงภาษาระดับล่าง) แต่ภาษา Java จะถูกใช้ที่นี่เพื่อแสดงวิธีการปรับให้เหมาะสมเหล่านี้ เป็นไปไม่ได้ที่จะครอบคลุมวิธีการเพิ่มประสิทธิภาพ JVM ทั้งหมดในส่วนนี้ ฉันหวังว่าการแนะนำเหล่านี้จะเป็นแรงบันดาลใจให้คุณเรียนรู้วิธีการเพิ่มประสิทธิภาพขั้นสูงเพิ่มเติมหลายร้อยวิธีและสร้างสรรค์นวัตกรรมในเทคโนโลยีคอมไพเลอร์
การกำจัดรหัสที่ตายแล้ว
การกำจัดโค้ดที่ไม่ทำงานตามชื่อคือการกำจัดโค้ดที่จะไม่มีวันถูกเรียกใช้งาน - นั่นคือโค้ด "เสีย"
หากคอมไพเลอร์พบคำสั่งที่ซ้ำซ้อนระหว่างการดำเนินการ คอมไพลเลอร์จะลบคำสั่งเหล่านี้ออกจากชุดคำสั่งการดำเนินการ ตัวอย่างเช่น ในรายการที่ 1 ตัวแปรตัวใดตัวหนึ่งจะไม่ถูกนำมาใช้หลังจากการกำหนดให้กับตัวแปรดังกล่าว ดังนั้นคำสั่งการกำหนดสามารถถูกละเว้นได้อย่างสมบูรณ์ในระหว่างการดำเนินการ สอดคล้องกับการดำเนินการในระดับไบต์โค้ด ค่าตัวแปรไม่จำเป็นต้องโหลดลงในรีจิสเตอร์ การไม่ต้องโหลดหมายความว่าจะใช้เวลา CPU น้อยลง จึงเร่งการเรียกใช้โค้ด ส่งผลให้แอปพลิเคชันเร็วขึ้นในที่สุด - หากเรียกใช้โค้ดการโหลดหลายครั้งต่อวินาที ผลการปรับให้เหมาะสมจะชัดเจนยิ่งขึ้น
รายการ 1 ใช้โค้ด Java เพื่อแสดงตัวอย่างการกำหนดค่าให้กับตัวแปรที่จะไม่มีวันถูกใช้
รายการ 1. รหัสการคัดลอกรหัสที่ไม่ทำงานมีดังนี้:
int timeToScaleMyApp (บูลีนไม่มีที่สิ้นสุดทรัพยากร) {
int reArchitect =24;
int patchByClustering =15;
int useZing =2;
ถ้า (endlessOfResources)
ส่งคืน reArchitect + useZing;
อื่น
กลับมาใช้Zing;
-
ในระหว่างเฟสโค้ดไบต์ ถ้าตัวแปรถูกโหลดแต่ไม่เคยใช้ คอมไพลเลอร์สามารถตรวจจับและกำจัดโค้ดที่ไม่ทำงาน ดังแสดงในรายการที่ 2 หากคุณไม่เคยดำเนินการโหลดนี้มาก่อน คุณสามารถประหยัดเวลาของ CPU และปรับปรุงความเร็วในการดำเนินการของโปรแกรมได้
รายการ 2. รหัสการคัดลอกโค้ดที่ได้รับการปรับปรุงมีดังนี้:
int timeToScaleMyApp (บูลีนไม่มีที่สิ้นสุดทรัพยากร) {
int reArchitect =24; //ลบการดำเนินการที่ไม่จำเป็นออกที่นี่...
int useZing =2;
ถ้า (endlessOfResources)
ส่งคืน reArchitect + useZing;
อื่น
กลับมาใช้Zing;
-
การกำจัดความซ้ำซ้อนเป็นวิธีการเพิ่มประสิทธิภาพที่ช่วยปรับปรุงประสิทธิภาพของแอปพลิเคชันโดยการลบคำแนะนำที่ซ้ำกัน
การปรับให้เหมาะสมหลายๆ อย่างพยายามกำจัดคำสั่งการข้ามระดับคำสั่งของเครื่อง (เช่น JMP ในสถาปัตยกรรม x86) คำสั่งการกระโดดจะเปลี่ยนการลงทะเบียนตัวชี้คำสั่ง ดังนั้นจะเบี่ยงเบนกระแสการทำงานของโปรแกรม คำสั่ง Jump นี้เป็นคำสั่งที่ใช้ทรัพยากรมากเมื่อเทียบกับคำสั่ง ASSEMBLY อื่นๆ นั่นเป็นเหตุผลที่เราต้องการลดหรือกำจัดคำสั่งประเภทนี้ การฝังโค้ดเป็นวิธีการเพิ่มประสิทธิภาพที่ใช้งานได้จริงและเป็นที่รู้จักกันดีในการขจัดคำแนะนำในการถ่ายโอน เนื่องจากการรันคำสั่ง Jump นั้นมีราคาแพง การฝังเมธอดเล็กๆ ที่มักเรียกว่าเล็กๆ เข้าไปในเนื้อหาของฟังก์ชันจะก่อให้เกิดประโยชน์มากมาย รายการที่ 3-5 แสดงให้เห็นถึงประโยชน์ของการฝัง
รายการ 3. คัดลอกวิธีการเรียกรหัส รหัสมีดังนี้:
int whenToEvaluateZing(int y){ กลับ daysLeft(y)+ daysLeft(0)+ daysLeft(y+1);}
รายการ 4. รหัสการคัดลอกวิธีการที่เรียกว่ามีดังต่อไปนี้:
int daysLeft(int x){ ถ้า(x ==0) return0; มิฉะนั้นจะส่งคืน x -1;}
รายการ 5. รหัสคัดลอกวิธีการอินไลน์มีดังนี้:
int เมื่อจะประเมิน Zing (int y) {
อุณหภูมิภายใน =0;
ถ้า(y==0)
อุณหภูมิ +=0;
อื่น
อุณหภูมิ += y -1;
ถ้า(0==0)
อุณหภูมิ +=0;
อื่น
อุณหภูมิ +=0-1;
ถ้า(y+1==0)
อุณหภูมิ +=0;
อื่น
อุณหภูมิ +=(y +1)-1;
อุณหภูมิกลับ;
-
ในรายการ 3-5 เราจะเห็นว่าเมธอดขนาดเล็กถูกเรียกสามครั้งในเนื้อหาของเมธอดอื่น และสิ่งที่เราต้องการแสดงให้เห็นคือ: ค่าใช้จ่ายในการฝังเมธอดที่ถูกเรียกลงในโค้ดโดยตรงจะน้อยกว่าการรันการกระโดดสามครั้ง ต้นทุนของ คำแนะนำในการโอน
การฝังวิธีการที่ไม่ค่อยถูกเรียกอาจไม่สร้างความแตกต่างมากนัก แต่การฝังวิธีที่เรียกว่า "hot" (วิธีการที่มักถูกเรียก) สามารถนำมาซึ่งการปรับปรุงประสิทธิภาพได้มาก โค้ดที่ฝังไว้มักจะสามารถปรับให้เหมาะสมเพิ่มเติมได้ ดังแสดงในรายการที่ 6
รายการ 6. หลังจากที่โค้ดถูกฝังแล้ว การเพิ่มประสิทธิภาพเพิ่มเติมสามารถทำได้โดยการคัดลอกโค้ดดังต่อไปนี้:
int whenToEvaluateZing(int y){ ถ้า(y ==0)ส่งคืน y; elseif(y ==-1)ส่งคืน y -1; elsereturn y + y -1;}
การเพิ่มประสิทธิภาพลูป
การเพิ่มประสิทธิภาพลูปมีบทบาทสำคัญในการลดต้นทุนเพิ่มเติมในการดำเนินการกับเนื้อความของลูป ค่าใช้จ่ายเพิ่มเติมในที่นี้หมายถึงการข้ามที่มีราคาแพง การตรวจสอบเงื่อนไขจำนวนมาก และไปป์ไลน์ที่ไม่ได้รับการปรับให้เหมาะสม (นั่นคือ ชุดคำสั่งที่ไม่มีการดำเนินการจริงและใช้รอบ CPU เพิ่มเติม) การปรับแต่งลูปให้เหมาะสมมีหลายประเภท ต่อไปนี้เป็นการปรับแต่งลูปยอดนิยมบางส่วน:
การรวมเนื้อความของลูป: เมื่อเนื้อความของวงที่อยู่ติดกันสองตัวดำเนินการวนซ้ำในจำนวนเท่ากัน คอมไพลเลอร์จะพยายามรวมเนื้อความของวงทั้งสองเข้าด้วยกัน หากเนื้อความของลูปสองตัวเป็นอิสระจากกันโดยสมบูรณ์ พวกมันก็สามารถดำเนินการพร้อมกันได้ (ขนานกัน)
Inversion Loop: โดยพื้นฐานที่สุด คุณจะแทนที่ while loop ด้วย do- While loop ลูป do- While นี้ถูกวางไว้ภายในคำสั่ง if การแทนที่นี้จะลดการกระโดดสองครั้ง แต่จะเพิ่มการตัดสินแบบมีเงื่อนไข ซึ่งจะเป็นการเพิ่มจำนวนโค้ด การปรับให้เหมาะสมประเภทนี้เป็นตัวอย่างที่ดีของการแลกเปลี่ยนทรัพยากรมากขึ้นเพื่อโค้ดที่มีประสิทธิภาพมากขึ้น - คอมไพลเลอร์จะชั่งน้ำหนักต้นทุนและผลประโยชน์ และทำการตัดสินใจแบบไดนามิกในขณะรันไทม์
จัดระเบียบเนื้อหาลูปใหม่: จัดระเบียบเนื้อหาลูปใหม่เพื่อให้สามารถจัดเก็บเนื้อหาลูปทั้งหมดไว้ในแคชได้
ขยายเนื้อหาลูป: ลดจำนวนการตรวจสอบและการข้ามเงื่อนไขของลูป คุณสามารถคิดได้ว่านี่เป็นการดำเนินการวนซ้ำ "อินไลน์" หลายครั้งโดยไม่ต้องทำการตรวจสอบตามเงื่อนไข การคลายตัวของลูปยังทำให้เกิดความเสี่ยงด้วย เนื่องจากอาจลดประสิทธิภาพโดยส่งผลกระทบต่อไปป์ไลน์และการดึงคำสั่งที่ซ้ำซ้อนจำนวนมาก อีกครั้งที่มันขึ้นอยู่กับคอมไพเลอร์ที่จะตัดสินใจว่าจะคลายลูปเนื้อหาในขณะรันไทม์หรือไม่ และจะคุ้มค่าที่จะคลายหากจะนำการปรับปรุงประสิทธิภาพที่ดียิ่งขึ้นมา
ข้างต้นเป็นภาพรวมว่าคอมไพเลอร์ในระดับไบต์โค้ด (หรือระดับต่ำกว่า) สามารถปรับปรุงประสิทธิภาพของแอปพลิเคชันบนแพลตฟอร์มเป้าหมายได้อย่างไร สิ่งที่เราได้พูดคุยไปแล้วคือวิธีการเพิ่มประสิทธิภาพทั่วไปและเป็นที่นิยม เนื่องจากพื้นที่มีจำกัด เราจึงยกตัวอย่างง่ายๆ เพียงบางส่วนเท่านั้น เป้าหมายของเราคือการกระตุ้นความสนใจของคุณในการศึกษาเชิงลึกเกี่ยวกับการเพิ่มประสิทธิภาพผ่านการสนทนาง่ายๆ ข้างต้น
สรุป: จุดสะท้อนและประเด็นสำคัญ
เลือกคอมไพเลอร์ที่แตกต่างกันตามวัตถุประสงค์ที่แตกต่างกัน
1. ล่ามเป็นรูปแบบที่ง่ายที่สุดในการแปลไบต์โค้ดเป็นคำสั่งของเครื่อง การใช้งานจะขึ้นอยู่กับตารางค้นหาคำสั่ง
2. คอมไพลเลอร์สามารถปรับให้เหมาะสมตามตัวนับประสิทธิภาพ แต่ต้องใช้ทรัพยากรเพิ่มเติมบางอย่าง (แคชโค้ด เธรดการปรับให้เหมาะสม ฯลฯ)
3. คอมไพเลอร์ไคลเอนต์สามารถนำการปรับปรุงประสิทธิภาพ 5 ถึง 10 เท่าเมื่อเปรียบเทียบกับล่าม
4. คอมไพเลอร์ฝั่งเซิร์ฟเวอร์สามารถนำมาซึ่งการปรับปรุงประสิทธิภาพประมาณ 30% ถึง 50% เมื่อเทียบกับคอมไพเลอร์ฝั่งไคลเอ็นต์ แต่ต้องใช้ทรัพยากรมากขึ้น
5. การรวบรวมหลายชั้นรวมข้อดีของทั้งสองอย่างเข้าด้วยกัน ใช้การคอมไพล์ฝั่งไคลเอ็นต์เพื่อให้เวลาตอบสนองเร็วขึ้น และจากนั้นใช้คอมไพเลอร์ฝั่งเซิร์ฟเวอร์เพื่อปรับโค้ดที่เรียกบ่อยให้เหมาะสม
มีหลายวิธีที่เป็นไปได้ในการเพิ่มประสิทธิภาพโค้ดที่นี่ งานสำคัญของคอมไพเลอร์คือการวิเคราะห์วิธีการปรับให้เหมาะสมที่เป็นไปได้ทั้งหมด จากนั้นชั่งน้ำหนักต้นทุนของวิธีการปรับให้เหมาะสมต่างๆ เทียบกับการปรับปรุงประสิทธิภาพที่ได้รับจากคำสั่งเครื่องจักรขั้นสุดท้าย