4 คะแนน โดย GN⁺ 2024-10-19 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

การใช้ประโยชน์จาก 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 ความคิดเห็น

 
GN⁺ 2024-10-19
ความคิดเห็นบน 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” ภาษานั้นถูกเรียกแบบนั้นจริงหรือ?

    • ไม่ใช่ แค่เป็น Go assembly
      ไวยากรณ์มีที่มาจาก Plan 9 ก็จริง แต่เราเรียกมันว่า Go assembly
      ดู https://go.dev/doc/asm
    • เป็นคำถามที่สมเหตุสมผล ตอนแรกผมก็คิดว่าถูกแล้วเหมือนกัน เพราะคิดว่าคนที่ค้นคว้าหัวข้อนี้คงไม่น่าจะพลาดเรื่องแบบนี้ และถ้ารู้จักโปรเจกต์อยู่บ้าง ชื่อนั้นก็ดูมีเหตุผลในระดับหนึ่ง
      แต่ยิ่งค้นต่อ ก็ยิ่งรู้สึกว่านี่น่าจะเป็น ภาพหลอนของ LLM
      เอกสารรูปแบบ assembly ไม่ได้ตั้งชื่อเฉพาะให้มัน แค่เรียกว่า go assembler
      แหล่งที่มาของภาพหลอนนี้น่าจะเป็นย่อหน้าแรก: “แอสเซมเบลอร์อิงตามสไตล์อินพุตของแอสเซมเบลอร์ Plan 9 และ… เอกสารฉบับนี้อธิบายสรุปไวยากรณ์ ความแตกต่าง และลักษณะเฉพาะเมื่อเขียนโค้ด assembly ที่โต้ตอบกับ Go”
    • มันไม่มีชื่อเฉพาะต่างหาก Plan 9 คือระบบปฏิบัติการ และสไตล์ไวยากรณ์ assembly นี้มีที่มาจากแอสเซมเบลอร์ที่ใช้ในระบบปฏิบัติการนั้น
      คล้ายกับการพูดว่า “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

    • มันได้ผลจริง Go ทำให้เกิดความคาดหวังว่าภาษาโปรแกรมใหม่ควรมี cross-compilation เป็นความสามารถพื้นฐาน และในเวลานั้นแทบไม่มีภาษาไหนทำเรื่องนี้ได้ดี
    • เท่าที่ทราบ SpiderMonkey ก็ทำสิ่งที่เกือบเหมือนกันมาประมาณ 25 ปี แล้ว และคิดว่า JavaScript VM อื่น ๆ ก็น่าจะทำเช่นกัน
  • มีประโยคว่า “ต้องการฟังก์ชันสำหรับทำ SIMD operation กับ slice” เลยสงสัยว่าจริง ๆ แล้วใช้อย่างไร
    ค้นทั้งบทความแล้วก็ไม่พบ operation ที่ทำกับ slice เลย
    แก้ไข: เจอในเอกสารที่ลิงก์ไว้แล้ว: https://pkg.go.dev/github.com/pehringer/simd#pkg-index
    โดยพื้นฐานแล้ว ถ้ามี slice 2 อันที่อยากบวกกัน ก็ใช้ SIMD ประมวลผลแบบขนานแทน for loop ได้ เช่น simd.AddInt32(slice1, slice2, result)

    • ควรใส่ตัวอย่างไว้ในเอกสารหรือ repository สักหน่อย ถ้าดู โค้ด benchmark ช่วงล่างของไฟล์นี้จะมีตัวอย่าง:
      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/

    • ในความเป็นจริง น่าจะถือได้ว่าชิปทุกตัวหลังราวปี 2008 รองรับ SSE4.1
  • ดูเหมือนผู้เขียนจะสับสนตรงนี้ เลยฝากลิงก์อ้างอิงไว้: https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs

  • ผมก็กำลังจะพูดเหมือนกันว่านี่ดูเหมือนเป็น ผลจากการที่ LLM ตีความโค้ดผิด
    การที่รู้จักคำว่า Plan 9 และเจาะลึกลงไปถึงแอสเซมบลี แต่กลับไม่รู้ว่าตัวเองกำลังลงไปอยู่ในน้ำแบบไหนนั้น ผมนึกภาพคำอธิบายอื่นแทบไม่ออกเลย เห็นคนอื่นคิดเหมือนกันก็เลยรู้ว่าไม่ได้คิดไปคนเดียว
    ถ้านี่ถูกต้อง ก็หวังว่าผู้เขียนจะไม่รู้สึกอายหรือรู้สึกว่า “โดนจับได้” แต่พูดตรง ๆ เพราะเราจะได้เรียนรู้จากมันด้วย ผมอยากมั่นใจมากขึ้นกับกรณีประเภท “LLM โผล่ร่องรอย” แบบนี้ แต่ต่อให้ดูชัดเจนแค่ไหน ก็แทบไม่เคยเห็นคนยอมรับกันเลย
    แน่นอนว่าในกรณีนี้มันไม่ได้ชัดเจน และเป็นแค่การคาดเดาที่รีบร้อนและตัดสินไปมากเท่านั้น

    • พูดตรง ๆ เหรอ? ไม่ ความโอหังที่คิดว่า LLM เป็นทางลัดที่ทำให้ไม่ต้องทำ งานที่ควรทำให้ถูกต้อง นั้น ควรต้องอาย
      เรื่องแบบนี้ทำให้หงุดหงิดจริง ๆ
  • เนื้อหานี้เกินระดับผมไปนิดหน่อย แต่ชอบวิธีที่บทความพาผู้อ่านเดินไปด้วยกัน
    นี่คงไม่ใช่ความพยายามครั้งแรกแบบนี้ใช่ไหม? น่าจะมี Gopher ที่กระหาย SIMD อยู่เป็นสิบ ๆ คนเลยทีเดียว แพตเทิร์นที่พบบ่อยกว่าคือใช้ CGO หรือเปล่า?

    • ปัญหาของ cgo คือ overhead ของการเรียกฟังก์ชันสูง ควรใช้กับงานที่เป็นก้อนค่อนข้างใหญ่เท่านั้น
      การเรียกฟังก์ชันแอสเซมบลีจาก Go ถูกกว่ามาก
      ที่ https://pkg.go.dev/github.com/grailbio/base/simd มีงานของผมในแนวทางนี้อยู่
    • ผมคิดว่าคนคงลองทำกันมาตั้งนานแล้วแน่ ๆ จำได้ว่าเมื่อเกือบ 10 ปีก่อนเคยคุยโทรศัพท์กับพี่/น้องของผมตอนที่เขาพยายามทำไลบรารี SIMD ด้วย Go (ที่ Skype หรือเปล่านะ?)
      ถ้าจำไม่ผิด ตอนนั้น คำสั่ง AVX หลายตัวแม้แต่จะเข้ารหัสในแอสเซมเบลอร์ Plan 9 ของ Go ก็ยังไม่ได้ ต้องเข้ารหัสเป็นไบต์เอง [0]
      ไลบรารีที่สมบูรณ์ที่สุดเท่าที่ผมเคยเห็น แม้จะไม่เคยใช้เอง คือใช้แฮ็กที่ค่อนข้างเรียบร้อย โดยใช้ CGO บางส่วนแต่หลบ overhead นั้นได้ [1]
      [0]: https://github.com/slimsag/rand/blob/f1e8d464c0021a391d5cd64...
      [1]: https://github.com/alivanz/go-simd/
    • ข้อเสนอให้เพิ่มสิ่งนี้เข้าไปใน standard library ถูกปฏิเสธไปแล้ว แต่ช่วยให้เข้าใจฉากหลังได้: https://github.com/golang/go/issues/53171
  • ถ้าจะเขียนโปรแกรมแอสเซมบลีของ Go แนะนำให้ดู Avo(https://github.com/mmcloughlin/avo)
    มันให้ type safety และตรวจสอบบางอย่างให้ว่า output เป็นแอสเซมบลีที่ถูกต้อง สามารถจัดสรรรีจิสเตอร์แบบไดนามิกให้ได้ และทำให้ไม่ต้องคำนวณเองอย่างขนาด stack กับ frame
    ยังจัดการรายละเอียด calling convention ให้ได้ด้วย ทำให้โหลด argument ไปยังรีจิสเตอร์หรือตำแหน่งที่ต้องการได้ง่าย
    เมื่อเร็ว ๆ นี้ผมเพิ่งพอร์ตแอสเซมบลี amd64 ทั้งหมดในไลบรารี crypto ของ Go ไปเป็น Avo ซึ่งเป็นไลบรารีที่มีประโยชน์มากสำหรับงานแบบนี้