HN เปิดเผย: บันทึก Go Plan9
(pehringer.info)การใช้ประโยชน์จาก concurrency และ parallelism ของ Go
- แนะนำโปรเจกต์ที่ต้องการเพิ่มความสามารถในการคำนวณตัวเลขด้วยการใช้ประโยชน์จาก concurrency และ parallelism ของ Go
- สามารถใช้คำสั่ง SIMD (Same Instruction Multiple Data) เพื่อทำการคำนวณแบบขนานในระดับฮาร์ดแวร์ได้
- คอมไพเลอร์ของ Go ไม่ได้ใช้ SIMD และเนื่องจากหาแพ็กเกจ SIMD แบบ general-purpose ที่เหมาะสมไม่ได้ จึงตัดสินใจพัฒนาแพ็กเกจขึ้นมาเอง
ภาษาแอสเซมบลี Plan9
- Go ใช้ภาษาแอสเซมบลีของตัวเองชื่อ Plan9 โดยเป็นการนำคำสั่งและรีจิสเตอร์ของแพลตฟอร์มเฉพาะมาปรับเล็กน้อยแล้วใช้งาน
- x86 Plan9 และ ARM Plan9 แตกต่างกัน
- อธิบายวิธีใช้งานพื้นฐานผ่านตัวอย่างง่าย ๆ ของ Plan9
ตัวอย่าง Plan9
- อธิบายการประกาศฟังก์ชันพื้นฐานและวิธีใช้งานของ Plan9 ผ่านไฟล์
AddInts_amd64.sและmain.go - อธิบายวิธีเก็บอาร์กิวเมนต์ของฟังก์ชันและค่าที่ส่งกลับไว้บนสแตกตาม calling convention ของ Go
แผนการออกแบบแพ็กเกจ
- ออกแบบแพ็กเกจที่ให้เลเยอร์ abstraction แบบบางสำหรับงาน SIMD ด้าน arithmetic และ bit operation
- สร้างแพ็กเกจภายในที่มี implementation ของ Plan9 แยกตามสถาปัตยกรรม และตั้งค่าผ่านฟังก์ชันเริ่มต้น
ตัวอย่าง SIMD
- อธิบายวิธีใช้ SIMD ผ่านตัวอย่างฟังก์ชัน x86 SIMD Plan9
- แสดงวิธีตรวจสอบการรองรับ SSE และการทำงานบวก
float32ผ่านไฟล์Supported_amd64.sและAddFloat32_amd64.s
ประสิทธิภาพและอนาคต
- จากกราฟที่แสดงความต่างด้านประสิทธิภาพระหว่าง implementation แบบซอฟต์แวร์ของ Go กับ implementation แบบ Plan9 SIMD พบว่าความเร็วเพิ่มขึ้นประมาณ 200-450%
- หวังว่าบันทึกนี้จะเป็นแรงบันดาลใจให้กับโปรเจกต์ที่ใช้ Plan9 และ SIMD
# สรุปโดย GN⁺
- บทความนี้แนะนำวิธีเพิ่มประสิทธิภาพสูงสุดด้วยการใช้ประโยชน์จาก concurrency และ parallelism ของ Go
- อธิบายวิธีทำการคำนวณแบบขนานในระดับฮาร์ดแวร์ด้วยภาษาแอสเซมบลี Plan9 และคำสั่ง SIMD
- บทความนี้นำเสนอความเป็นไปได้ในการใช้ Plan9 และ SIMD ให้กับนักพัฒนา Go และอาจมีประโยชน์ต่อการสำรวจแนวทางใหม่ ๆ เพื่อเพิ่มประสิทธิภาพ
- โปรเจกต์ที่มีความสามารถคล้ายกันซึ่งแนะนำ ได้แก่ ไลบรารีรองรับ SIMD ของ Rust หรือไลบรารีที่เกี่ยวข้องกับ SIMD ของ C++
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ขอชี้ประเด็นเกี่ยวกับ Go assembly สักเล็กน้อย:
intเหล่านั้นบน amd64 จริง ๆ แล้วเป็น 64 บิตถ้าใช้
int32รายการพารามิเตอร์จะถูกจัดแนวตาม word แต่มีข้อควรระวังอยู่ บนระบบ 64 บิต ค่าที่คืนกลับจะเริ่มที่ออฟเซ็ตที่จัดแนวแบบ doubleword เสมอNOSPLITถูกนิยามไว้ในtextflag.hซึ่งคอมไพเลอร์ Go จัดเตรียมให้อัตโนมัติ อย่างไรก็ตามเท่าที่อ่านมา ดูเหมือนว่าNOSPLITจะถูกเคารพเฉพาะในฟังก์ชันruntime.XXเท่านั้น ดังนั้นในกรณีนี้จึงไม่ได้ทำอะไร และก็ไม่จำเป็นด้วยNOSPLITหมายถึงการบอกคอมไพเลอร์ว่าอย่าแทรกโค้ดสำหรับตรวจสอบว่าสแต็กอาจล้นจนต้อง split หรือไม่ หากเป็นฟังก์ชันที่ไม่ต้องใช้พื้นที่สแต็ก ในทางเทคนิคก็ไม่จำเป็น และโดยพื้นฐานแล้วมันมีไว้เพื่อป้องกันไม่ให้โค้ดตรวจสอบนั้นถูกฉีดเข้าไปในตัวฟังก์ชันที่ทำหน้าที่ตรวจสอบ stack splitting เองสำหรับคนที่สงสัยว่าทำไม
4ถึงหมายถึง “NOSPLIT” และทำไมถึงบอกว่าจำเป็น: โดยทั่วไปหลังขนาดเฟรม (พารามิเตอร์หลังNOSPLIT) จะตามด้วยขนาดอาร์กิวเมนต์ และทั้งสองคั่นด้วยเครื่องหมายลบนี่ไม่ใช่การลบ แต่เป็นไวยากรณ์เฉพาะตัวเท่านั้น ขนาดเฟรม
$24-8หมายความว่าฟังก์ชันมีเฟรม 24 ไบต์ และถูกเรียกด้วยอาร์กิวเมนต์ 8 ไบต์ที่อยู่ในเฟรมของผู้เรียกถ้าไม่ได้ระบุ
NOSPLITให้กับTEXTต้องระบุขนาดอาร์กิวเมนต์เสมอ สำหรับฟังก์ชัน assembly ที่มี prototype ของ Go นั้นgo vetจะตรวจสอบว่าขนาดอาร์กิวเมนต์ถูกต้องหรือไม่ที่มา: https://go.dev/doc/asm
ที่ว่า “Go ใช้ภาษา assembly ภายในของตัวเองชื่อ Plan9” ภาษานั้นถูกเรียกแบบนั้นจริงหรือ?
ไวยากรณ์มีที่มาจาก Plan 9 ก็จริง แต่เราเรียกมันว่า Go assembly
ดู https://go.dev/doc/asm
แต่ยิ่งค้นต่อ ก็ยิ่งรู้สึกว่านี่น่าจะเป็น ภาพหลอนของ LLM
เอกสารรูปแบบ assembly ไม่ได้ตั้งชื่อเฉพาะให้มัน แค่เรียกว่า
go assemblerแหล่งที่มาของภาพหลอนนี้น่าจะเป็นย่อหน้าแรก: “แอสเซมเบลอร์อิงตามสไตล์อินพุตของแอสเซมเบลอร์ Plan 9 และ… เอกสารฉบับนี้อธิบายสรุปไวยากรณ์ ความแตกต่าง และลักษณะเฉพาะเมื่อเขียนโค้ด assembly ที่โต้ตอบกับ Go”
คล้ายกับการพูดว่า “GNU Compiler Collection ใช้ภาษา assembly ภายในของตัวเองชื่อ Unix”
ถ้าสงสัยว่าทำไมทีม Go ถึงเลือกรูปแบบ assembly เฉพาะตัวแบบนี้ Rob Pike เคยพูดถึง การออกแบบ Go assembler ในงานนำเสนอปี 2016 [1][2]
แก่นหลักน่าจะเป็นข้อสังเกตว่า assembly language ส่วนใหญ่โดยรวมคล้ายกัน จึงทำ common assembly language ที่ช่วยให้ “คุยกับระดับต่ำสุดของเครื่องได้โดยไม่ต้องเรียนไวยากรณ์ใหม่”
นอกจากนี้ยังทำให้สามารถสร้างแอสเซมเบลอร์โดยอัตโนมัติจาก PDF คู่มือคำสั่งของสถาปัตยกรรมใหม่เป็นอินพุตได้ด้วย
[1]: https://www.youtube.com/watch?v=KINIAgRpkDA
[2]: https://go.dev/talks/2016/asm.slide#1
มีประโยคว่า “ต้องการฟังก์ชันสำหรับทำ SIMD operation กับ slice” เลยสงสัยว่าจริง ๆ แล้วใช้อย่างไร
ค้นทั้งบทความแล้วก็ไม่พบ operation ที่ทำกับ slice เลย
แก้ไข: เจอในเอกสารที่ลิงก์ไว้แล้ว: https://pkg.go.dev/github.com/pehringer/simd#pkg-index
โดยพื้นฐานแล้ว ถ้ามี slice 2 อันที่อยากบวกกัน ก็ใช้ SIMD ประมวลผลแบบขนานแทน
forloop ได้ เช่นsimd.AddInt32(slice1, slice2, result)https://github.com/pehringer/simd/blob/main/simd_test.go
ส่วนนี้เกี่ยวข้องกับโปรเซสเซอร์ที่ Go รองรับ (1) การรองรับ x64 ขั้นพื้นฐานรวม SSE และ SSE2 อยู่ด้วย
แต่ไม่แน่ใจว่าคอมไพเลอร์ Go สร้างสิ่งนั้นจริงหรือไม่ ต่างจากคอมไพเลอร์ที่ซับซ้อนมากอย่าง gcc ซึ่งให้ความสำคัญกับประสิทธิภาพสูงสุด คอมไพเลอร์ Go ชอบแนว Wirth (2) ที่เรียบง่ายและคอมไพล์ได้เร็ว
(1) https://go.dev/wiki/MinimumRequirements#amd64
(2) https://irreal.org/blog/?p=7075
https://smartgo.blog/2024/01/06/niklaus-wirth/
ดูเหมือนผู้เขียนจะสับสนตรงนี้ เลยฝากลิงก์อ้างอิงไว้: https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs
ผมก็กำลังจะพูดเหมือนกันว่านี่ดูเหมือนเป็น ผลจากการที่ LLM ตีความโค้ดผิด
การที่รู้จักคำว่า Plan 9 และเจาะลึกลงไปถึงแอสเซมบลี แต่กลับไม่รู้ว่าตัวเองกำลังลงไปอยู่ในน้ำแบบไหนนั้น ผมนึกภาพคำอธิบายอื่นแทบไม่ออกเลย เห็นคนอื่นคิดเหมือนกันก็เลยรู้ว่าไม่ได้คิดไปคนเดียว
ถ้านี่ถูกต้อง ก็หวังว่าผู้เขียนจะไม่รู้สึกอายหรือรู้สึกว่า “โดนจับได้” แต่พูดตรง ๆ เพราะเราจะได้เรียนรู้จากมันด้วย ผมอยากมั่นใจมากขึ้นกับกรณีประเภท “LLM โผล่ร่องรอย” แบบนี้ แต่ต่อให้ดูชัดเจนแค่ไหน ก็แทบไม่เคยเห็นคนยอมรับกันเลย
แน่นอนว่าในกรณีนี้มันไม่ได้ชัดเจน และเป็นแค่การคาดเดาที่รีบร้อนและตัดสินไปมากเท่านั้น
เรื่องแบบนี้ทำให้หงุดหงิดจริง ๆ
เนื้อหานี้เกินระดับผมไปนิดหน่อย แต่ชอบวิธีที่บทความพาผู้อ่านเดินไปด้วยกัน
นี่คงไม่ใช่ความพยายามครั้งแรกแบบนี้ใช่ไหม? น่าจะมี Gopher ที่กระหาย SIMD อยู่เป็นสิบ ๆ คนเลยทีเดียว แพตเทิร์นที่พบบ่อยกว่าคือใช้ CGO หรือเปล่า?
การเรียกฟังก์ชันแอสเซมบลีจาก Go ถูกกว่ามาก
ที่ https://pkg.go.dev/github.com/grailbio/base/simd มีงานของผมในแนวทางนี้อยู่
ถ้าจำไม่ผิด ตอนนั้น คำสั่ง AVX หลายตัวแม้แต่จะเข้ารหัสในแอสเซมเบลอร์ Plan 9 ของ Go ก็ยังไม่ได้ ต้องเข้ารหัสเป็นไบต์เอง [0]
ไลบรารีที่สมบูรณ์ที่สุดเท่าที่ผมเคยเห็น แม้จะไม่เคยใช้เอง คือใช้แฮ็กที่ค่อนข้างเรียบร้อย โดยใช้ CGO บางส่วนแต่หลบ overhead นั้นได้ [1]
[0]: https://github.com/slimsag/rand/blob/f1e8d464c0021a391d5cd64...
[1]: https://github.com/alivanz/go-simd/
ถ้าจะเขียนโปรแกรมแอสเซมบลีของ Go แนะนำให้ดู Avo(https://github.com/mmcloughlin/avo)
มันให้ type safety และตรวจสอบบางอย่างให้ว่า output เป็นแอสเซมบลีที่ถูกต้อง สามารถจัดสรรรีจิสเตอร์แบบไดนามิกให้ได้ และทำให้ไม่ต้องคำนวณเองอย่างขนาด stack กับ frame
ยังจัดการรายละเอียด calling convention ให้ได้ด้วย ทำให้โหลด argument ไปยังรีจิสเตอร์หรือตำแหน่งที่ต้องการได้ง่าย
เมื่อเร็ว ๆ นี้ผมเพิ่งพอร์ตแอสเซมบลี amd64 ทั้งหมดในไลบรารี crypto ของ Go ไปเป็น Avo ซึ่งเป็นไลบรารีที่มีประโยชน์มากสำหรับงานแบบนี้