ในโลกของการพัฒนาซอฟต์แวร์ เรามักจะเชื่อใจให้คอมไพเลอร์แปลงรหัสของเราเป็นคำสั่งเครื่องจักรที่มีประสิทธิภาพสูงสุด แต่สิ่งที่เกิดขึ้นเมื่อการเพิ่มประสิทธิภาพอัตโนมัติเหล่านี้กลับทำให้โค้ดของเราช้าลง? การอภิปรายล่าสุดในหมู่ผู้พัฒนาซอฟต์แวร์ได้เปิดเผยกรณีที่น่าประหลาดใจที่การเพิ่มประสิทธิภาพของคอมไพเลอร์—โดยเฉพาะอย่างยิ่งตารางจัมป์—สามารถลดประสิทธิภาพลงอย่างมีนัยสำคัญแทนที่จะปรับปรุงให้ดีขึ้น
การถดถอยของประสิทธิภาพที่คาดไม่ถึง
การอภิปรายเริ่มต้นจากที่นักพัฒนาได้ทำการวัดประสิทธิภาพของการคำนวณความยาวลำดับ UTF-8 ซึ่งฟังก์ชันที่ใช้การนับบิตศูนย์นำด้วยความช่วยเหลือของฮาร์ดแวร์กลับมีประสิทธิภาพต่ำอย่างน่าประหลาดใจ โค้ดดังกล่าวประมวลผลข้อมูลข้อความได้เพียง 438-462 เมกะไบต์ต่อวินาที ซึ่งน้อยกว่าวิธีการแบบแบรนชิ่งแบบง่ายๆ ที่สามารถจัดการได้มากกว่า 2000 เมกะไบต์ต่อวินาที ตัวการที่แท้จริงกลายเป็นว่าเป็นการเพิ่มประสิทธิภาพของคอมไพเลอร์ที่แทนที่คำสั่งแบรนชิ่งด้วยตารางจัมป์—ตารางค้นหาที่แมปค่าต่างๆ ไปยังที่อยู่โค้ด ในขณะที่ตารางจัมป์มักจะหลีกเลี่ยงบทลงโทษจากการทำนายแบรนช แต่ในกรณีเฉพาะนี้ พวกมันได้นำรูปแบบการเข้าถึงหน่วยความจำที่สร้างความเสียหายต่อประสิทธิภาพมากกว่าการใช้แบรนช
คอมไพเลอร์สมัยใหม่มีความสามารถอย่างน่าประทับใจในการสร้างไมโครออปติไมเซชันการจัดการบิตจากโค้ดที่เป็นแบบแผน พวกมันยังดีในด้านมาโครออปติไมเซชันเชิงโครงสร้างขนาดใหญ่ อย่างไรก็ตาม มีดินแดนรกร้างสำหรับการเพิ่มประสิทธิภาพของคอมไพเลอร์ระหว่างไมโครออปติไมเซชันและมาโครออปติไมเซชัน ซึ่งประสิทธิผลของการเพิ่มประสิทธิภาพของคอมไพเลอร์มีความน่าเชื่อถือน้อยกว่ามาก
ข้อสังเกตนี้เกิดเสียงสะท้อนกับนักพัฒนาหลายคนที่เคยพบกับหลุมพรางการเพิ่มประสิทธิภาพที่คล้ายกัน ความคิดเห็นนี้เน้นย้ำว่าคอมไพเลอร์ทำงานได้ดีเยี่ยมทั้งในการจัดการบิตขนาดเล็กและการเปลี่ยนแปลงเชิงโครงสร้างขนาดใหญ่ แต่ต้องดิ้นรนกับการเพิ่มประสิทธิภาพขนาดกลางที่ผลประโยชน์มีความคาดเดาได้น้อยกว่า
การเปรียบเทียบประสิทธิภาพ: Jump Tables กับ Branching
- การประมวลผล UTF-8 ด้วย jump tables: 438-462 MB/s
- การประมวลผล UTF-8 ด้วย branching: 2000+ MB/s
- การปรับปรุงประสิทธิภาพด้วย
-fno-jump-tables: เร็วขึ้นประมาณ 4.5 เท่า
ทำความเข้าใจช่องว่างการเพิ่มประสิทธิภาพของคอมไพเลอร์
นักพัฒนาในการอภิปรายระบุสาเหตุหลายประการว่าทำไมการเพิ่มประสิทธิภาพของคอมไพเลอร์บางครั้งจึงให้ผลลัพธ์ตรงข้าม คอมไพเลอร์ใช้กฎการเปลี่ยนแปลงเชิงกำหนดที่ออกแบบมาให้ทำงานได้ดีในกรณีการใช้งานที่หลากหลาย แต่พวกมันไม่สามารถคำนึงถึงทุกสถานการณ์เฉพาะได้ คอมไพเลอร์ที่แตกต่างกันอาจเลือกกลยุทธ์การเพิ่มประสิทธิภาพที่แตกต่างกันสำหรับโค้ดเดียวกัน— GNU g++ สำหรับ AArch64 ไม่ได้ปล่อยตารางจัมป์ที่มีปัญหาออกมาเหมือนที่ clang++ ทำ นอกจากนี้ยังมีการแข่งขันระหว่างการเพิ่มประสิทธิภาพ ซึ่งการใช้ออปติไมเซชันหนึ่งอาจป้องกันไม่ให้ออปติไมเซชันอื่นที่อาจดีกว่าถูกนำไปใช้
ผลกระทบต่อประสิทธิภาพแตกต่างกันอย่างมีนัยสำคัญ across สถาปัตยกรรมฮาร์ดแวร์ สิ่งที่ทำงานได้ดีบนโปรเซสเซอร์ x86_64 รุ่นใหม่ที่มีแคชขนาดใหญ่อาจมีประสิทธิภาพต่ำบนระบบที่มีลักษณะหน่วยความจำที่แตกต่างกัน เช่น CPU MIPS ของ Nintendo 64 ที่มีการจัดการแคชด้วยซอฟต์แวร์และมีความหน่วงของ RDRAM สูง ความไวต่อสถาปัตยกรรมนี้อธิบายว่าทำไมการตัดสินใจเพิ่มประสิทธิภาพที่ดูสมเหตุสมผลในทางทฤษฎีจึงล้มเหลวในทางปฏิบัติ
ความแตกต่างในพฤติกรรมของคอมไพเลอร์
- Clang++ 18.1.3 (AArch64): สร้าง jump table โดยค่าเริ่มต้น
- GNU g++ (AArch64): ไม่สร้าง jump table
- แฟล็กสำหรับปิดการใช้งาน:
-fno-jump-tables
ผลกระทบในทางปฏิบัติสำหรับโค้ดที่สำคัญต่อประสิทธิภาพ
การอภิปรายเปิดเผยบทเรียนสำคัญหลายประการสำหรับนักพัฒนาที่ทำงานกับโค้ดที่สำคัญต่อประสิทธิภาพ การปิดใช้งานตารางจัมป์อย่างง่ายๆ ด้วยแฟลกคอมไพเลอร์เช่น -fno-jump-tables บางครั้งสามารถปรับปรุงประสิทธิภาพได้อย่างมาก ดังที่แสดงให้เห็นโดยการวัดประสิทธิภาพ UTF-8 ที่กระโดดจาก ~450 เมกะไบต์ต่อวินาที เป็นมากกว่า 2000 เมกะไบต์ต่อวินาที ภูมิปัญญาดั้งเดิมที่ว่าให้หลีกเลี่ยงการแบรนชนั้นไม่ถูกต้องเสมอไป—รูปแบบการแบรนชที่คาดเดาได้สามารถมีประสิทธิภาพเหนือกว่าโค้ดแบบไม่มีแบรนชที่มีการพึ่งพาข้อมูลที่ไม่เหมาะสม
นักพัฒนาควรเข้าถึงการเพิ่มประสิทธิภาพของคอมไพเลอร์ในฐานะเครื่องมือที่มีประโยชน์มากกว่าที่จะเป็นวิธีแก้ปัญหาเวทมนตร์ ดังที่ผู้แสดงความคิดเห็นหนึ่งคนระบุว่า มันไม่ใช่เวทมนตร์ ซึ่งทำให้โค้ดแต่ละส่วนเร็วขึ้น มันเป็นเพียงชุดของกฎการเปลี่ยนแปลงโค้ดเชิงกำหนด ซึ่งโดยปกติแล้วจะทำให้โค้ดเร็วขึ้นเมื่อพิจารณาจากชุดกรณีการใช้งานขนาดใหญ่ แต่มันไม่ได้พิสูจน์แล้วว่าพวกมันจะทำเช่นนั้นเสมอไป มุมมองนี้ส่งเสริมให้นักพัฒนาตรวจสอบการตัดสินใจเพิ่มประสิทธิภาพผ่านการวัดมาตรฐาน มากกว่าที่จะสมมติว่าคอมไพเลอร์จะเลือกแนวทางที่เร็วที่สุดเสมอ
การสนทนาเกี่ยวกับความล้มเหลวในการเพิ่มประสิทธิภาพของคอมไพเลอร์ทำหน้าที่เป็นข้อเตือนใจที่มีคุณค่าว่าการปรับแต่งประสิทธิภาพต้องการการตรวจสอบเชิงประจักษ์ ในขณะที่คอมไพเลอร์มีความซับซ้อนอย่างไม่น่าเชื่อในการเพิ่มประสิทธิภาพโค้ด พวกมันยังคงทำงานภายใต้ข้อจำกัดที่สามารถนำไปสู่การตัดสินใจที่ไม่ได้ดีที่สุดในกรณีเฉพาะ นักพัฒนาที่เข้าใจทั้งกลยุทธ์การเพิ่มประสิทธิภาพของคอมไพเลอร์และลักษณะประสิทธิภาพของฮาร์ดแวร์ของพวกเขาจะมีความพร้อมที่ดีกว่าในการเขียนโค้ดที่มีประสิทธิภาพสูงอย่างแท้จริง ข้อคิดสำคัญคือการเพิ่มประสิทธิภาพของคอมไพเลอร์เป็นเครื่องมือที่ต้องเข้าใจและชี้นำ ไม่ใช่ไม้กายสิทธิ์ที่ต้องพึ่งพาอย่างสุ่มสี่สุ่มห้า
