นักพัฒนาถกเถียงอนาคตของการเขียนโปรแกรม SIMD ขณะที่ฟังก์ชันเวกเตอร์เผชิญข้อจำกัดของคอมไพเลอร์

ทีมชุมชน BigGo
นักพัฒนาถกเถียงอนาคตของการเขียนโปรแกรม SIMD ขณะที่ฟังก์ชันเวกเตอร์เผชิญข้อจำกัดของคอมไพเลอร์

โลกของการประมวลผลประสิทธิภาพสูงกำลังมีการถกเถียงอย่างร้อนแรงเกี่ยวกับวิธีการทำให้การเขียนโปรแกรม SIMD (Single Instruction, Multiple Data) เข้าถึงได้ง่ายขึ้นสำหรับนักพัฒนา แม้ว่าโปรเซสเซอร์สมัยใหม่จะมีพลังการประมวลผลแบบขนานที่น่าทึ่งผ่านคำสั่งเวกเตอร์ แต่การใช้พลังนี้จริงๆ ยังคงเป็นเรื่องที่น่าหงุดหงิดสำหรับโปรแกรมเมอร์ส่วนใหญ่

การเคลื่อนไหวของ Portable SIMD ได้รับแรงผลักดัน

นักพัฒนาจำนวนมากขึ้นเรื่อยๆ กำลังผลักดันให้มีโซลูชัน portable SIMD ที่ดีกว่าซึ่งทำงานข้ามสถาปัตยกรรมโปรเซสเซอร์ต่างๆ ภาษาเช่น Rust กำลังทดลองกับ std::simd ในขณะที่ C และ C++ เสนอ vector extensions ที่สัญญาว่าจะเขียนโค้ด SIMD ครั้งเดียวและรันได้ทุกที่ ความน่าสนใจนั้นชัดเจน - แทนที่จะต้องจำ intrinsics ที่เข้าใจยากเช่น _mm_add_ps สำหรับ x86 หรือ vaddq_f32 สำหรับ ARM นักพัฒนาก็สามารถใช้ตัวดำเนินการทางคณิตศาสตร์ที่คุ้นเคยเช่น + และ - บนประเภทเวกเตอร์ได้

อย่างไรก็ตาม ความเป็นจริงนั้นซับซ้อนกว่า นักพัฒนาหลายคนรายงานว่าแม้โซลูชันแบบพกพาเหล่านี้จะทำงานได้ดีสำหรับการดำเนินการทางคณิตศาสตร์พื้นฐาน แต่มันจะล้มเหลวอย่างรวดเร็วเมื่อต้องจัดการกับคำสั่งเฉพาะทางหรือโค้ดที่สำคัญต่อประสิทธิภาพ ปัญหาตัวหารร่วมน้อยหมายความว่าฟีเจอร์ขั้นสูงที่มีอยู่ในโปรเซสเซอร์เฉพาะมักจะไม่สามารถใช้ผ่านการแยกแยะแบบพกพาได้

SIMD intrinsics คือฟังก์ชันระดับต่ำที่แมปโดยตรงกับคำสั่งเวกเตอร์ของโปรเซสเซอร์ ช่วยให้ควบคุมการดำเนินการแบบขนานได้อย่างแม่นยำ แต่ต้องใช้โค้ดเฉพาะแพลตฟอร์ม

แนวทางการเขียนโปรแกรม SIMD ทั่วไป

แนวทาง ความสามารถในการพกพา ประสิทธิภาพ ความง่ายในการใช้งาน การรองรับของคอมไพเลอร์
Raw Intrinsics ต่ำ สูงสุด ยาก รองรับทั่วไป
Portable SIMD ( Rust std::simd ) สูง ดี ปานกลาง จำกัด
C/C++ Vector Extensions ปานกลาง ดี ปานกลาง ดี
Auto-vectorization สูง แปรผัน ง่าย ดี
Libraries ( Highway , ISPC ) สูง ดีมาก ปานกลาง ดี
ภาพประกอบแนวคิดของ SIMD ที่เน้นความแตกต่างระหว่าง SIMD และการประมวลผลแบบคำสั่งเดียวแบบดั้งเดิม
ภาพประกอบแนวคิดของ SIMD ที่เน้นความแตกต่างระหว่าง SIMD และการประมวลผลแบบคำสั่งเดียวแบบดั้งเดิม

การ Auto-Vectorization ของคอมไพเลอร์ยังคงไม่เพียงพอ

แม้จะมีการพัฒนามาหลายทศวรรษ การทำ vectorization อัตโนมัติโดยคอมไพเลอร์ยังคงไม่น่าเชื่อถือสำหรับสถานการณ์ในโลกแห้งหลายกรณี แม้ว่าคอมไพเลอร์สมัยใหม่เช่น Clang 16+ และ GCC 13+ จะปรับปรุงขึ้นอย่างมาก แต่พวกมันยังคงดิ้นรนกับการควบคุมการไหลที่ซับซ้อน รูปแบบการเข้าถึงหน่วยความจำ และสิ่งใดก็ตามที่เกินกว่าลูปง่ายๆ นักพัฒนารายงานอย่างสม่ำเสมอว่าเมื่อประสิทธิภาพสำคัญจริงๆ - ในเอนจินกราฟิก ตัวถอดรหัสวิดีโอ หรือปริมาณงานการเรียนรู้ของเครื่อง - โค้ด SIMD ที่เขียนด้วยมือยังคงมีประสิทธิภาพเหนือกว่าเวอร์ชันที่สร้างโดยคอมไพเลอร์

ปัญหาพื้นฐานคือคอมไพเลอร์ต้องระมัดระวังเพื่อให้แน่ใจว่าถูกต้อง ในขณะที่โปรแกรมเมอร์มนุษย์สามารถตัดสินใจอย่างมีข้อมูลเกี่ยวกับการจัดตำแหน่งข้อมูล รูปแบบการเข้าถึงหน่วยความจำ และโครงสร้างอัลกอริทึมที่ปลดล็อกประสิทธิภาพที่ดีกว่ามาก

โมเดลการเขียนโปรแกรม GPU แสดงเส้นทางที่แตกต่าง

มุมมองที่น่าสนใจที่เกิดขึ้นจากชุมชนชี้ไปที่โมเดลการเขียนโปรแกรม GPU เช่น CUDA เป็นโซลูชันที่เป็นไปได้ ต่างจากการเขียนโปรแกรม CPU SIMD, CUDA ช่วยให้นักพัฒนาเขียนโค้ดที่ดูเหมือนโค้ดเธรดเดียวปกติ แต่ขยายขนาดอัตโนมัติข้ามการกำหนดค่าฮาร์ดแวร์ต่างๆ วิธีการนี้ประสบความสำเร็จมากว่าสองทศวรรษ แต่ผู้ขาย CPU และนักเขียนภาษา/คอมไพเลอร์ยังไม่เต็มใจที่จะใช้กระบวนทัศน์ที่คล้ายกัน

เมื่อ 20 ปีที่แล้ว มันชัดเจนมากสำหรับทุกคนที่ต้องเขียนการทำงานแบบขนานที่เข้ากันได้แบบไปข้างหน้า/ย้อนหลังว่าสิ่งที่ nvidia เรียกว่า SIMT คือวิธีการที่ถูกต้อง ฉันคิดว่าผู้ผลิตฮาร์ดแวร์ CPU และนักเขียนภาษา/คอมไพเลอร์ดื้อรั้นเกินไปจนจะใช้เวลาสิบปีในการตามทัน ฉันคิดผิด

ความท้าทายคือปริมาณงาน CPU นั้นแตกต่างจากปริมาณงาน GPU โดยพื้นฐาน CPU ต้องจัดการทุกอย่างตั้งแต่สตริงสั้นไปจนถึงตรรกะการแยกสาขาที่ซับซ้อน ในขณะที่ GPU เก่งในปัญหาแบบขนานที่น่าอายกับชุดข้อมูลขนาดใหญ่

SIMT (Single Instruction, Multiple Thread) คือโมเดลการเขียนโปรแกรมของ NVIDIA ที่เธรดหลายตัวดำเนินการคำสั่งเดียวกันแต่บนข้อมูลต่างกัน คล้ายกับ SIMD แต่มีความยืดหยุ่นมากกว่าสำหรับเส้นทางการดำเนินการที่แยกออกจากกัน

โซลูชันอุตสาหกรรมเกิดขึ้นแม้จะมีข้อจำกัดของภาษา

ในขณะที่นักออกแบบภาษากำลังถกเถียงเกี่ยวกับการแยกแยะที่สมบูรณ์แบบ โซลูชันที่ใช้งานได้จริงกำลังเกิดขึ้นจากอุตสาหกรรม ไลบรารีเช่น Highway ของ Google และ ISPC ของ Intel ให้ความสามารถในการส่งต่อแบบไดนามิกที่สร้างเวอร์ชันหลายเวอร์ชันของฟังก์ชันเดียวกันสำหรับความสามารถของโปรเซสเซอร์ต่างๆ จากนั้นเลือกเวอร์ชันที่ดีที่สุดโดยอัตโนมัติในขณะรันไทม์

วิธีการเหล่านี้แสดงถึงจุดกึ่งกลาง - พวกมันพกพาได้มากกว่า intrinsics ดิบแต่ยืดหยุ่นกว่าการแยกแยะระดับภาษา อย่างไรก็ตาม พวกมันยังคงต้องการให้นักพัฒนาคิดในแง่ของการดำเนินการเวกเตอร์และเข้าใจฮาร์ดแวร์พื้นฐานเพื่อให้ได้ประสิทธิภาพที่เหมาะสมที่สุด

ความสามารถด้านความกว้างของเวกเตอร์ตามสถาปัตยกรรม

  • SSE (x86): เวกเตอร์ 128 บิต (4x float32, 2x float64)
  • AVX2 (x86): เวกเตอร์ 256 บิต (8x float32, 4x float64)
  • AVX-512 (x86): เวกเตอร์ 512 บิต (16x float32, 8x float64)
  • ARM NEON: เวกเตอร์ 128 บิต (4x float32, 2x float64)
  • ARM SVE: เวกเตอร์ความกว้างแปรผัน (128-2048 บิต)
  • RISC-V RVV: เวกเตอร์ความกว้างแปรผัน (ขึ้นอยู่กับการนำไปใช้งาน)

เส้นทางข้างหน้ายังคงไม่ชัดเจน

ชุมชนยังคงแบ่งแยกเกี่ยวกับวิธีการที่ดีที่สุดในการก้าวไปข้างหน้า บางคนสนับสนุนการสนับสนุนระดับภาษาที่ดีกว่าซึ่งจะทำให้การเขียนโปรแกรม SIMD เป็นธรรมชาติเหมือนการเขียนโปรแกรมปกติ คนอื่นๆ โต้แย้งว่า SIMD เป็นโดเมนเฉพาะทางโดยธรรมชาติที่ต้องการความรู้และเครื่องมือเฉพาะทาง

สิ่งที่ชัดเจนคือสถานการณ์ปัจจุบันทิ้งประสิทธิภาพที่สำคัญไว้บนโต๊ะ โปรเซสเซอร์สมัยใหม่มียูนิตเวกเตอร์ที่สามารถเพิ่มความเร็วได้ 4 เท่า 8 เท่า หรือแม้แต่ 16 เท่าสำหรับปริมาณงานที่เหมาะสม แต่การเข้าถึงประสิทธิภาพนี้ต้องการความเชี่ยวชาญลึกใน intrinsics เฉพาะโปรเซสเซอร์หรือหวังว่า auto-vectorization ที่ซับซ้อนขึ้นเรื่อยๆ แต่ยังคงไม่น่าเชื่อถือจะทำงานสำหรับกรณีการใช้งานเฉพาะของคุณ

ขณะที่โปรเซสเซอร์ยังคงเพิ่มความสามารถเวกเตอร์มากขึ้นและปริมาณงานมีความไวต่อประสิทธิภาพมากขึ้น การแก้ไขความตึงเครียดระหว่างการเข้าถึงได้และประสิทธิภาพนี้จะกลายเป็นสิ่งสำคัญยิ่งขึ้นสำหรับอนาคตของการประมวลผลประสิทธิภาพสูง

อ้างอิง: The messy reality of UMD (vector) functions