Go 1.27 portable SIMD: เร่งงานเวกเตอร์โดยไม่ผูก CPU
งานบวกตัวเลขทีละสมาชิกเหมือนขนของเข้าคลังทีละกล่อง ทั้งที่รถเข็นยกได้หลายกล่องพร้อมกันครับ CPU สมัยใหม่มี SIMD หรือ “คำสั่งเดียวทำกับข้อมูลหลายช่อง” มานาน แต่ Go เคยต้องพึ่ง assembly หรือ API ที่ผูกกับสถาปัตยกรรมมากเกินไป Go 1.27 จึงทดลองแพ็กเกจ simd แบบพกพา เพื่อเขียนแกนคำนวณครั้งเดียวแล้วให้ runtime เลือก AVX, AVX2, AVX512, NEON หรือ WebAssembly SIMD ตามเครื่องจริง
WHY: ความเร็วที่พกข้ามเครื่องได้มีค่ากว่า benchmark เครื่องเดียว
ถ้าเขียนเฉพาะ AVX512 โปรแกรมอาจเร็วมากบนเซิร์ฟเวอร์หนึ่ง แต่ต้องมีเส้นทางสำรองเมื่อไปรันบน ARM หรือเครื่องที่ไม่มี instruction นั้น API ใหม่ซ่อนความกว้างของเวกเตอร์ออกจาก type system ชนิดอย่าง simd.Float32s จึงถาม Len() ตอนรันได้ แล้วโหลดหรือเก็บข้อมูลผ่าน slice นักพัฒนาไม่ต้องฝังเลขจำนวน lane ลงใน algorithm
อุปมาเหมือนร้านจัดของที่ปรับขนาดถาดตามรถส่งของ วันนี้รถรับแปดกล่องก็ใช้ถาดแปด พรุ่งนี้รถรับสิบหกก็เปลี่ยนถาด แต่สูตรคัดแยกเดิมยังอยู่ จุดสำคัญคือแพลตฟอร์มที่ยังไม่มี SIMD ก็รันด้วย emulation ได้ จึงไม่ต้องแตก feature เป็นคนละผลิตภัณฑ์
HOW: คิดเป็นสามช่วง ไม่ใช่แค่เปลี่ยน loop
ช่วงแรกคือ full vectors: โหลด slice ทีละ Len() แล้วคำนวณ เช่น MulAdd สำหรับ inner product ช่วงสองคือ tail หรือเศษท้ายที่จำนวนสมาชิกไม่เต็มเวกเตอร์ ใช้ partial load/store และต้องแน่ใจว่าสมาชิกนอกช่วงไม่ไหลเข้าผลรวม ช่วงสุดท้ายคือ reduction ซึ่ง Go 1.27 ยังไม่มี ReduceSum; ตัวอย่างทางการจึงเก็บเวกเตอร์กลับเป็น slice แล้วรวมแบบ scalar ข้อจำกัดนี้ทำให้บางงานยังไม่คุ้มย้าย
การเปิดใช้ต้องตั้ง GOEXPERIMENT=simd ตอน build ชื่อ API และประสิทธิภาพยังเปลี่ยนได้ จึงควรแยก implementation ไว้หลังฟังก์ชันเดียว เก็บ scalar reference ไว้เทียบความถูกต้อง และ benchmark ทั้งโหมด SIMD กับ GODEBUG=simd=0 บน amd64, arm64 และสภาพแวดล้อม CI ที่ทีมใช้จริง
จุดพลาดที่ควรทดสอบ
- slice ว่าง, สั้นกว่า vector และยาวไม่หาร
Len()ลงตัว - ค่า
NaN, infinity และ signed zero ในงาน floating point - integer overflow กับ saturated arithmetic ว่าตรง domain หรือไม่
- ผลลัพธ์ที่ต่างเพราะลำดับการบวก floating point เปลี่ยน
- allocator หรือการ copy ที่เพิ่มขึ้นจนกินกำไรจาก SIMD
อย่าเริ่มจากเปลี่ยนทุก loop ให้เริ่มจาก profiler หา kernel ที่กิน CPU จริง แล้วทำ benchmark แบบข้อมูลใกล้ production ถ้าคอขวดคือ I/O, allocation หรือ branch กระจัดกระจาย SIMD อาจไม่ช่วย ต่อให้กราฟ microbenchmark ดูดี
Recap
- เปิดใช้เฉพาะจุดร้อนที่ profiler ชี้
- แยก scalar reference และทดสอบ tail ทุกขนาด
- วัดบน CPU หลายตระกูล รวมโหมด emulation
- ถือ
simdเป็น experiment ยังไม่ผูก public API ของระบบกับมัน - ตรวจความคลาดเคลื่อน floating point ก่อนดูแต่ throughput