2 คะแนน โดย GN⁺ 2024-08-14 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Spice เป็นโปรเจกต์วิจัยที่พยายามใช้ heartbeat scheduling ใน Zig เพื่อเพิ่มความเป็นไปได้ในการรันฟังก์ชันแบบขนาน โดยคงโอเวอร์เฮดไว้ต่ำกว่า 1ns
  • การออกแบบหลักคือใช้ fork เพื่อระบุงานที่เธรดอื่นสามารถจัดการได้ แต่ถ้าไม่มีเธรดใดหยิบไปทำ join จะให้เธรดปัจจุบันรันเองโดยตรงในรูปแบบ fallback
  • ในเบนช์มาร์กการหาผลรวม binary tree 100 ล้านโหนด Rayon มีโอเวอร์เฮดบน 1 เธรดประมาณ 15ns และบน 16 เธรดเร็วกว่า baseline ประมาณ 4.5 เท่า แต่ Spice บน 16 เธรดได้ความเร็วเพิ่มขึ้นประมาณ 11 เท่าเมื่อเทียบกับ baseline แทบจะเต็มจำนวน
  • สำหรับงานที่เวลารันสั้นมาก เช่น 1000 โหนด มีกรณีที่ Rayon บน 32 เธรดทำให้เวลารวม ช้าลง 60 เท่า ส่วน Spice หากประเมินว่าไม่จำเป็นต้องประมวลผลขนาน จะทำให้เธรดเพิ่มเติมหลับไว้และไม่เริ่ม multithreading
  • implementation ปัจจุบันยังขาดการทดสอบ เอกสาร การรองรับ array/slice และเบนช์มาร์กเพิ่มเติม ใช้ @panic อยู่มาก และผู้เขียนระบุว่าก่อนใช้ใน production ต้องรับรู้ถึง ข้อจำกัดจำนวนมาก

ปัญหาที่ Spice ต้องการแก้

  • Spice เป็นโปรเจกต์ที่พยายามทำให้การประมวลผลขนานแบบละเอียดมากใน Zig มีโอเวอร์เฮดต่ำ
  • เป้าหมายคือให้ผู้ใช้ไม่ต้องคอยกังวลอยู่เสมอว่าเมื่อเพิ่ม parallelization แล้วโปรแกรมจะช้าลงหรือไม่
  • หากต้องการรีดประสิทธิภาพสูงสุดยังต้องมีเบนช์มาร์กที่ละเอียด แต่โดยทั่วไป Spice ถูกออกแบบให้เมื่อเพิ่ม parallelism แล้วแทบไม่มีโอเวอร์เฮดจริง
  • โปรเจกต์นี้เป็น โปรเจกต์วิจัยเป็นหลัก และหากพิจารณาใช้งาน production ควรตรวจสอบข้อจำกัดก่อน
  • ในอัปเดตเดือนกันยายน 2024 มีการแนะนำ Chili ซึ่งเป็นพอร์ตแนวคิดนี้ไปยัง Rust

วิธีใช้และ API หลัก

  • ฟังก์ชันขนานของ Spice รับ *spice.Task เป็นพารามิเตอร์เพื่อใช้ประสานงาน
  • การเรียกแบบ recursive หรือการเรียกฟังก์ชันที่ทำ parallelize ได้ ต้องไม่เรียกโดยตรง แต่ควรเรียกผ่าน t.call
  • fork ใช้ตั้งค่างานที่เธรดอื่นสามารถรันได้
  • หลัง fork ฟังก์ชันควรทำงานที่มีความหมายเองโดยตรงด้วย
  • join รอว่าเธรดอื่นทำงานเสร็จแล้วหรือไม่ แต่สามารถคืนค่า null ได้
    • null เป็นสัญญาณว่าเธรดอื่นไม่ได้หยิบงานนั้นไปทำ
    • ในกรณีนี้ เธรดปัจจุบันต้องรันงานนั้นเองโดยตรง

การออกแบบที่ว่า “งานทั้งหมดไม่ได้มาจาก queue”

  • แนวคิดหลักของ Spice คือ งานทั้งหมดไม่ได้เริ่มจาก queue
  • fork แจ้งว่ามีงานที่เธรดอื่นสามารถรันได้ แต่ถ้าเธรดอื่นยุ่งอยู่ เธรดปัจจุบันจะจัดการเหมือนการรันแบบลำดับเดิม
  • เมื่อไม่มีความเป็นไปได้ในการรันขนาน สิ่งที่ Spice ทำบน hot path คือประมาณ push/pop ใน queue และไม่ได้เข้าไปดูรายการใน queue จริง ๆ
  • การประสานงานจริงกับเธรดอื่นเกิดขึ้นที่ heartbeat แบบคงที่
    • ประมาณทุก 100 ไมโครวินาที เธรดจะตรวจ queue งานปัจจุบัน
    • ส่งงานที่อยู่บนสุดของ queue ให้เธรดอื่นที่กำลังรออยู่
    • เพราะ heartbeat มีความถี่ต่ำ ต่อให้ใช้เวลาหลายร้อย ns โอเวอร์เฮดโดยรวมก็ยังเล็ก

เปรียบเทียบกับเบนช์มาร์ก Rayon

  • การหาผลรวม binary tree 100 ล้านโหนดเป็นกรณีที่การคำนวณจริงเร็วมาก จึงทำให้โอเวอร์เฮดของ parallel framework โผล่ชัด
  • ทั้ง Rayon ของ Rust และ Spice ถูกเปรียบเทียบในรูปแบบ API fork/join ที่อ่านและทำความเข้าใจได้ง่าย
  • ในเบนช์มาร์ก Rayon วัดโอเวอร์เฮดได้ประมาณ 15ns
    • เพิ่มจาก 7.48ns เป็น 22.99ns
    • บน 4 เธรดอยู่ในระดับที่กลับมามีประสิทธิภาพเท่าการรันแบบลำดับ แต่ใช้ CPU มากขึ้น 4 เท่า
    • บน 16 เธรด Rayon เร็วขึ้นประมาณ 14 เท่าเมื่อเทียบกับตัวมันเอง และเร็วขึ้นประมาณ 4.5 เท่าเมื่อเทียบกับ baseline
  • Spice เมื่อเพิ่มจาก 1 เธรดเป็น 16 เธรด แสดงความเร็วเพิ่มขึ้นประมาณ 11 เท่า
    • scaling แย่กว่า Rayon เล็กน้อย แต่เพราะโอเวอร์เฮดต่ำ ความเร็วที่เพิ่มขึ้นเมื่อเทียบกับ baseline จึงคงอยู่แทบเต็มจำนวน
  • เบนช์มาร์กรันบนอินสแตนซ์ c4-standard-16 ของ Google Cloud ในสภาพแวดล้อม 16 คอร์
  • ยังไม่ชัดเจนว่าทำไม Zig baseline จึงเร็วกว่า Rust baseline ประมาณ 2 เท่า
    • ตาม compiled assembly Rust เก็บ register 5 ตัวไว้บน stack ส่วน Zig เก็บ 3 ตัว

พฤติกรรมกับงานขนาดเล็ก

  • การหาผลรวม binary tree 1000 โหนดเป็นงานที่สั้นมาก โดยเวลารันรวมอยู่ในระดับไม่กี่ไมโครวินาที
  • ในกรณีนี้ Rayon มีโอเวอร์เฮดสูงขึ้นเป็นประมาณ 19ns และยิ่งเพิ่มเธรด ประสิทธิภาพยิ่งแย่ลง
  • เมื่อใช้ 32 เธรดบนเครื่อง 16 คอร์ เวลารันรวม ช้าลง 60 เท่า
    • ไม่สามารถฟันธงได้ว่าเครื่อง 32 คอร์จะเกิดการชะลอแบบเดียวกัน
    • ถึงอย่างนั้น พฤติกรรม scaling แบบนี้ก็ถูกประเมินว่าน่ากังวล
  • การตัดสินใจทำ parallel processing แบบดั้งเดิมมักอยู่ในรูป “คุ้มค่าก็ต่อเมื่อมีงานมากพอ”
    • เกณฑ์ของ “งานมากพอ” อาจต้องหาโดยเบนช์มาร์กแยกตาม input
    • สำหรับ input อย่าง binary tree ที่ดูแค่ root แล้วไม่รู้ขนาดทั้งหมด การตัดสินว่าเป็นงานเล็กหรือไม่นั้นทำได้ยาก
    • หาก 90% ของปริมาณงานเป็น input ขนาดเล็ก การชะลออย่างสุดขั้วอาจเป็นปัญหา
    • เมื่อโปรแกรมพัฒนาไป เกณฑ์ของงานที่มากพอก็เปลี่ยนตาม
  • ในกรณี 1000 โหนดเดียวกัน Spice ประเมินว่าเวลารันสั้นเกินไปจึงไม่เริ่ม multithreading
    • เธรดเพิ่มเติมยังคงอยู่ในสถานะหลับ
    • คอร์จึงสามารถใช้รันโปรแกรมอื่นได้

ความแตกต่างระหว่าง work-stealing กับ Spice

  • Spice มีโมเดล fork/join ซึ่งโดยทั่วไปมัก implement ด้วย work-stealing
  • work-stealing ทั่วไปให้แต่ละเธรดมี local work queue และเมื่อ queue ว่างจะขโมยงานจากท้าย queue ของเธรดอื่น
  • ความไม่มีประสิทธิภาพของ work-stealing สรุปได้สามข้อ
    • งานทั้งหมดกลายเป็นรูปแบบ generic “dynamic function call” จึงมีต้นทุนของ dynamic dispatch
    • local queue ในทางปฏิบัติเป็น queue ที่ทุกเธรดสามารถขโมยได้ จึงต้องใช้ atomic operation
    • เมื่อเกิด contention ใน queue จะมี spinning และภายใต้เงื่อนไขบางอย่างอาจช้าลง 10–100 เท่า
  • Spice ลดความไม่มีประสิทธิภาพเหล่านี้โดยตรง
    • dynamic dispatch ของ work queue ใช้เฉพาะตอนส่งงานไปยังเธรดอื่น
    • งานที่ทำภายในเธรดเดียวใช้การเรียกฟังก์ชันปกติ
    • การ push ลง work queue จัดการด้วย stack pointer, stack frame ปัจจุบัน และการบันทึก register โดยไม่ synchronize กับเธรดอื่น
    • ไม่มี spinning เพราะไม่มี while loop ที่วนโดยไม่เรียก wait()

รายละเอียด implementation

  • การปรับแต่ง static dispatch

    • Spice มองว่างานที่ถูก fork ส่วนใหญ่จะไม่ถูกเธรดอื่นหยิบไปทำ จึงวาง path การรันโค้ดนั้นซ้ำไว้ภายในฟังก์ชัน
    • หากงานไม่ได้ถูกรันโดยเธรดอื่น โปรแกรมจะทำงานเหมือน เวอร์ชันลำดับ ที่เพิ่ม branch ที่คาดเดาได้ไม่กี่จุด
    • โครงสร้างนี้เป็นผลดีต่อการ optimize โค้ด เช่น inlining และต่อการทำงานของ CPU
  • สัญญาณ heartbeat โอเวอร์เฮดต่ำ

    • heartbeat scheduling ทำ scheduling ใน local ด้วยความถี่ต่ำ
    • ประมาณทุก 100 ไมโครวินาที เธรดจะดู local work queue แล้วส่งงานให้เธรดอื่น
    • หากใช้เวลา 100ns ทุก 100 ไมโครวินาที โอเวอร์เฮดรวมจะอยู่ราว 0.1%
    • แทนที่จะใช้ signal ของระบบปฏิบัติการ จะเรียก tick() แบบร่วมมือกัน
      • เมื่อใช้ helper t.call จะเรียก tick() ให้อัตโนมัติ
      • เธรด heartbeat แยกต่างหากจะเปลี่ยนค่า atomic heartbeat ของแต่ละเธรดจาก false เป็น true เป็นระยะ
      • tick() อ่านค่านี้และเมื่อเป็น true ก็รันโค้ด heartbeat
    • ฟังก์ชัน heartbeat ต้องถูกทำเครื่องหมายเป็น cold มิฉะนั้นโอเวอร์เฮดจะสูงขึ้นมาก
  • global mutex ที่ไม่มี contention

    • thread pool ของ Spice มี mutex เดี่ยวที่ถูก lock จากหลายจุด
    • global mutex จะเป็นปัญหาเมื่อเธรดถูก block จริง
    • ใน Spice ด้วย heartbeat โดยปกติจะมีเพียงเธรดเดียวที่รัน heartbeat
    • ขณะถือ lock จะไม่รันโค้ดของผู้ใช้ และป้องกันแค่การอ่าน/เขียนหน่วยความจำอย่างง่ายที่จบในเวลาคงที่
  • doubly linked list แบบไม่มี branch

    • Spice ใช้ doubly linked list เพื่อติดตาม work queue
    • fork() append ที่ท้าย, join() pop จากท้ายถ้ายังเหลืออยู่ และเมื่อส่งให้ background worker จะ pop จากด้านหน้า
    • append ทั่วไปต้องมีเงื่อนไขตรวจว่า list ว่างหรือไม่
    • Spice ใส่ sentinel head node ที่มีอยู่เสมอเพื่อทำให้ list ไม่ว่าง และทำ push/pop ได้โดยไม่มี branch
  • ลดการใช้ stack

    • Future มีสถานะ queued หรือ executing
    • heartbeat เปลี่ยน queued future เป็น executing
    • state เพิ่มเติมที่จำเป็นสำหรับสถานะ executing ถูกวางไว้ในโครงสร้างแยกที่ allocate จาก pool เพื่อลดการใช้ stack ของ queued future
    • ใช้รูปแบบ manual tagged union โดยแยกสถานะ queued/executing จากการที่ field แรก prev_or_null เป็น null หรือไม่
  • ส่งค่าผ่าน register

    • Task มี pointer ไปยัง owning worker และ pointer ไปยัง tail ของ work queue
    • LLVM มักส่ง struct ผ่าน stack ดังนั้น Spice จึงนิยาม callWithContext ที่รับ worker และ job_tail เป็นพารามิเตอร์ฟังก์ชันแยกกัน
    • ฟังก์ชันนี้จะถูกเรียกให้ inline เสมอ เพื่อให้พารามิเตอร์ pointer ถูกส่งผ่าน register

งานวิจัยพื้นฐานและงานที่เกี่ยวข้อง

  • Spice สร้างขึ้นบนพื้นฐานงานวิจัย heartbeat scheduling
  • “The best multicore-parallelization refactoring you've never heard of” เป็น paper ที่แนะนำแนวคิด heartbeat scheduling แบบสั้น ๆ โดยเน้น use case เดียว แต่เล่าในรูปแบบที่นำไป generalize ได้
    • วิธีแก้ของ paper นี้เปลี่ยนโค้ดเป็น continuation-passing style เพื่อสลับระหว่างการรันแบบลำดับและแบบขนาน
    • Spice เริ่มจากการทดลองแนวทางนี้ แต่พบว่าโอเวอร์เฮดเกิน 10ns
  • “Heartbeat scheduling: provable efficiency for nested parallelism” เป็น paper แรกที่แนะนำ heartbeat scheduling
    • มีข้อมูลเชิงแนวคิดจำนวนมาก แต่ implementation อิงการผสานกับ interpreter และเน้นการรับประกันเชิงทฤษฎีมากกว่า
  • “Task parallel assembly language for uncompromising parallelism” เป็นงานวิจัยต่อเนื่องที่ปรับปรุงประสิทธิภาพ heartbeat ด้วย custom assembly language และ OS signaling
    • ถูกประเมินว่ายากต่อการผสานเข้ากับภาษาที่มีอยู่

ข้อจำกัดปัจจุบัน

  • Spice อาจมี พฤติกรรมหยาบ ๆ เมื่อใช้งานผิด
    • โดยเฉพาะไวต่อวิธีใช้ fork และ join
    • ควรปรับปรุงด้วย compile-time check, debug-mode assertion และการเปลี่ยน API
  • มีโค้ด concurrency จำนวนมาก แต่ test coverage เป็น 0
  • ไม่มี native support สำหรับการประมวลผลแต่ละ element ของ array/slice ซึ่งเป็น use case ทั่วไปของการประมวลผลขนานแบบละเอียด
  • ยังขาดเอกสารที่ดีสำหรับอธิบายวิธีใช้งาน
  • ตอนนี้ทดสอบโดยเน้นเบนช์มาร์กขนาดเล็กเพียงตัวเดียว
    • แม้มองว่าเบนช์มาร์กนั้นควรมีความเป็นตัวแทน แต่การตรวจสอบผลลัพธ์ยังต้องมีเบนช์มาร์กเพิ่มเติม
  • ใช้ @panic มากในการจัดการ error
    • หากจะมองว่าเป็นไลบรารี Zig ที่เหมาะสม ต้องพิจารณาการจัดการ error case ให้มากขึ้น
  • ต้องมีเบนช์มาร์กและการทดสอบเพิ่มเติมว่าใน ReleaseSafe ของ Zig ทำงานได้ดีเพียงใด
  • codebase ทั้งหมดมีประมาณ 500 บรรทัด และปัจจุบันผู้เขียนไม่มีแผนพัฒนาอย่างจริงจังเพื่อปรับปรุง Spice เนื่องจากมีเวลาไม่พอ
  • สนับสนุนให้ปรับปรุงผ่าน fork หรือการ re-implement ในภาษาอื่น

1 ความคิดเห็น

 
GN⁺ 2024-08-14
ความคิดเห็นจาก Hacker News
  • การใช้งานนี้อิงกับแนวทางงานวิจัยล่าสุดที่เรียกว่า Heartbeat Scheduling โดย amortize ต้นทุนของการสร้าง parallelism เพื่อให้ได้สิ่งที่คล้ายกับการควบคุม granularity ของงาน แบบอัตโนมัติไดนามิก
    งานวิจัยที่เกี่ยวข้อง:
    (2018) Heartbeat Scheduling: Provable Efficiency for Nested Parallelism. https://www.andrew.cmu.edu/user/mrainey/papers/heartbeat.pdf
    (2021) Task Parallel Assembly Language for Uncompromising Parallelism. https://users.cs.northwestern.edu/~simonec/files/Research/pa...
    (2024) Compiling Loop-Based Nested Parallelism for Irregular Workloads. https://users.cs.northwestern.edu/~simonec/files/Research/pa...
    (2024) Automatic Parallelism Management. https://www.cs.cmu.edu/~swestric/24/popl24-par-manage.pdf

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

    • ผมเป็นผู้เขียน
      ผมรู้อยู่แล้วว่าบางคนอาจตอบสนองเชิงลบกับถ้อยคำนี้ แต่เจตนาคือช่วยให้เข้าใจดีขึ้นว่าควรใช้ Spice และ Rayon เมื่อไรและอย่างไรอย่างแม่นยำ
      แนะนำให้อ่านเอกสาร benchmark: https://github.com/judofyr/spice/blob/main/bench/README.md
      ปกติเวลาเปรียบเทียบโค้ด parallel เรามักเทียบแค่ implementation แบบลำดับ/ตัวอ้างอิง กับ implementation แบบ parallel ที่ใช้ทุกเธรด (16 เธรด) ในเคส 100M ตัวเลขของ Rayon คือเวอร์ชันลำดับ 7.48ns และ Rayon 1.64ns แล้วก็ง่ายที่จะสรุปว่า “Rayon เร็วกว่ากับปัญหานี้ 4.5 เท่า แต่ใช้ 16 เธรด จึงไม่ค่อยเหมาะ” คำพูดนั้นถูก แต่เรียนรู้ได้ยากว่าจะนำไปใช้กับปัญหาประเภทอื่นอย่างไร
      ถ้ารัน benchmark เดียวกันด้วยจำนวนเธรดหลายแบบ จะเห็นจุดที่น่าสนใจกว่า scheduler ของ Rayon ค่อนข้างเก่งในการแบ่งงานไปยังเธรดแยก แต่กลไกการรันงานทั้งหมดมี overhead ประมาณ 15ns ต่อให้โปรแกรมนี้เป็นตัวอย่างที่ไร้ประโยชน์โดยสิ้นเชิง เราก็ยังได้เรียนรู้ข้อเท็จจริงที่นำไปใช้ต่อได้: ถ้าจะใช้ Rayon หน่วยงานที่เล็กที่สุดน่าจะต้อง ใหญ่กว่าประมาณ 7ns เว้นแต่ว่าการลด latency รวมสำคัญกว่าการเสีย throughput รวม
      เอกสารของ Rayon ไม่ได้ให้ตัวเลขไว้ บอกเพียงว่า “ในเชิงแนวคิด การเรียก join() คล้ายกับการสร้างสองเธรดแล้วให้แต่ละเธรดรัน closure หนึ่งตัว แต่ implementation ต่างกันมากและมี overhead ต่ำมาก”: https://docs.rs/rayon/latest/rayon/fn.join.html
      ถ้าผมอยากทำให้เข้าใจผิด ผมคงพูดว่า “Spice เร็วขึ้น 10 เท่า ส่วน Rayon 4.5 เท่า ดังนั้น Spice เร็วกว่า Rayon สองเท่า” ไปแล้ว
    • ถ้า “overhead ต่ำกว่า 1 นาโนวินาที” จะเป็นข้อความการตลาดที่ชวนให้เข้าใจผิดได้ ค่า Spice แบบ 1 เธรด - implementation อ้างอิงแบบไม่ parallel ต้องมากกว่า 1ns
      ผลการทดสอบสนับสนุนคำกล่าวอ้างนี้: https://github.com/judofyr/spice/tree/main/bench
    • ผมคิดว่านี่ก็คล้ายกับ ตำแหน่งใน ecosystem ของ Rayon ที่ถูกอ้างถึงไม่ใช่หรือ
      เป็นโครงสร้างที่ต้องประมวลผลงานตั้งแต่หลักพันถึงหลักล้านงาน ต้องการ parallelize ให้มากที่สุดภายในคอร์หลักสิบ และไม่อยากให้ถูก scheduling overhead กินไป จึงสนใจ overhead ต่องาน
    • เมื่อวานตอนโพสต์บน Reddit ผมแสดงความกังวลเรื่อง benchmark ไว้
      benchmark อ้างว่า overhead 0.36ns ต่อการเรียก แต่รวมเฉพาะฟังก์ชันคำนวณเท่านั้น มีเธรดที่สองทำ scheduling อยู่ แต่ไม่ได้รวมอยู่ในตัวเลข overhead ดูเหมือนรันบนเครื่อง 8 คอร์แบบ hyperthreading หรือก็คือ 16 เธรด และถ้าสมมติว่า 3GHz ก็แปลตรงตัวว่า overhead หนึ่ง cycle
      เมื่อเพิ่มเธรด overhead จะเพิ่มขึ้นเพราะ lock contention ที่ 16 เธรดเพิ่มเป็น 3.6ns หรือ 10 เท่า ผมเดาเอาว่าถ้าเป็นเช่นนั้น overhead 0.36ns แปลว่ารวม lock ที่ไม่มี contention อยู่ด้วย ซึ่งเป็นไปไม่ได้ ในข้อมูล benchmark ยังมีจุดแปลก ๆ เพิ่มเติมด้วย อาจเป็นเพราะผมไม่เข้าใจว่าจริง ๆ แล้วมันวัดอะไร หรืออาจมีบั๊กในโค้ด benchmark
      ถ้าลองคูณค่าทั้งหมด ดูเหมือนจะวัดเวลาเป็นหน่วยมิลลิวินาที เมื่อคำนวณเวลา execution แล้วแปลงเป็นมิลลิวินาที ค่าจะออกมาเป็นจำนวนเต็ม ปกติ benchmarker ใช้ความละเอียดดีกว่านี้ไม่ใช่หรือ? อาจเป็นไปได้ว่าใช้แค่ time prog ทำให้ข้อมูลสกปรกมาก หรือเลือก metric ที่ไร้ประโยชน์โดยสิ้นเชิงสำหรับจุดประสงค์นี้
    • ถ้าอ่าน README จะเห็นว่าอธิบายอย่างละเอียดมากว่าข้อกล่าวอ้างในหัวเรื่องหมายถึงอะไร
      ไม่มีหัวข้อไหนที่ไม่มีช่องให้เข้าใจผิดเลย และหัวข้อนี้ก็โอเค สิ่งที่ผมรับไปคือเป็นไลบรารีที่มี latency ต่ำมากภายใต้เกณฑ์การวัดบางอย่าง และแค่ไปดูเกณฑ์นั้นใน README ก็พอ ค่อนข้างชัดเจน
  • ไม่ได้คุ้นเคยกับสาขานี้มากนัก แต่ชอบ โมเดลการทำงานพร้อมกัน ที่นำเสนอไว้ตรงนี้
    README ก็เขียนไว้ดีมาก แค่อ่านก็พอจับภาพได้ว่าเกิดอะไรขึ้น อย่างไรก็ตาม มีอยู่สองสามจุดที่ทำให้เอียงคอสงสัยอยู่บ้าง โชคดีที่โค้ดค่อนข้างอ่านง่าย

    • ถ้าบิลด์ด้วย zig build ในโหมด Debug จะได้ Baseline,3.92809172, Spice 1 thread,19.1012624
      ถ้าบิลด์ด้วย zig build --release=safe ในโหมด ReleaseSafe จะได้ Baseline,3.264224280000001, Spice 1 thread,3.78043278
      ดังนั้นถ้า Spice ไม่ใช่บิลด์แบบรีลีส ประสิทธิภาพจะตกค่อนข้างมาก ส่วน implementation อ้างอิงไม่ได้ช้าลงในระดับเดียวกันในโหมดบิลด์ Debug ของ Zig
      เวอร์ชันที่ใช้คือ zig 0.13.0
  • เป็นงานวิจัยที่น่าสนใจ และไม่ใช่แค่ตัวโค้ดเท่านั้น เหตุผลประกอบการตัดสินใจก็ดี เอกสารก็เขียนไว้ดี
    บทความปี 2018 เรื่อง Heartbeat Scheduling ก็น่าอ่าน: https://www.andrew.cmu.edu/user/mrainey/papers/heartbeat.pdf

  • รายการข้อจำกัดของโปรเจกต์: https://github.com/judofyr/spice?tab=readme-ov-file#limitati...

    • โปรเจกต์นี้ยอดเยี่ยม และผู้เขียนควรได้รับคำชมอย่างมากที่สละเวลาทำให้มันทำงานได้และนำมาแชร์กับชุมชน HN
      โดยทั่วไป HN ก็ขึ้นชื่อว่าเป็นที่ที่มักมีปฏิกิริยาวิจารณ์แรงหรือมองโลกแง่ร้ายเกินไป
      การที่ผู้เขียนยอมรับข้อจำกัดของโปรเจกต์ตัวเองก็เป็นเรื่องดี และช่วยกันความเหน็บแนมที่มักเจอได้ล่วงหน้าไปเกือบหมด
      ในส่วนที่ว่า “ขาดการทดสอบ: Spice มีโค้ด concurrent ที่ซับซ้อนอยู่มาก แต่ test coverage เป็น 0 ถ้าจะใช้ Spice กับงานสำคัญอย่างรับผิดชอบ เรื่องนี้ต้องปรับปรุง” ผมคิดว่า นอกเหนือจากการทดสอบความถูกต้องในการรันงานสำคัญแล้ว ถ้าเป็นไลบรารีที่ implement โค้ด concurrent ที่ซับซ้อน อย่างน้อยก็ควรมี regression test
      ในมุมของผู้ใช้ปลายทาง ผมสงสัยว่ามีอะไรรับประกันได้ว่าฟีเจอร์ที่ทำงานได้วันนี้จะไม่พังในวันพรุ่งนี้จาก regression แบบละเอียดอ่อนและร้ายกาจ
      SQLite มีโค้ดทดสอบและสคริปต์ทดสอบมากกว่าซอร์สโค้ด C ล้วน ๆ ถึง 590 เท่า https://www.sqlite.org/testing.html นอกจากความเสถียรและการพกพาได้แล้ว จุดนี้ก็เป็นหนึ่งในหลายเหตุผลที่ทำให้ SQLite กลายเป็นฐานข้อมูลฝังตัวมาตรฐานโดยพฤตินัยทั่วโลก
      เป็นตัวอย่างที่ค่อนข้างฝืนเปรียบเทียบแอปเปิลกับส้ม แต่ประเด็นทั่วไปยังคงใช้ได้อยู่ regression test ก่อให้เกิดเสถียรภาพและความน่าเชื่อถือของโปรเจกต์
      ที่ที่ผมทำงาน ถ้าจำเป็นต้องเลื่อนการทำ regression test พื้นฐานจริง ๆ ก็มักจะสร้าง ticket ติดตามผลไว้ใน epic เดียวกัน และอย่างน้อยต้องเขียนให้เสร็จก่อนปล่อยฟีเจอร์/epic
  • ตามคำอธิบาย ใช้ busy waiting ใน worker เพื่อให้ได้ latency ระดับนาโนวินาที
    ผมสงสัยว่า busy waiting จะสมจริงแค่ไหนในแอปพลิเคชันขนาดใหญ่ที่มีงานเป็นหมื่น ๆ งาน ถ้างานไม่ได้อิง thread แต่เป็น asynchronous ก็อาจเป็นไปได้ เพราะจะมีตัวรอเท่ากับขนาด N ของ thread pool ของ executor เท่านั้น ไม่ว่าอย่างไร โครงสร้างแบบนี้ก็น่าจะใช้พลังงานสูงขึ้น
    ในเรื่องที่เกี่ยวกัน ผมสงสัยมานานแล้วว่ามีวิธีให้ producer ของงานปลุก consumer ได้เร็วขึ้นโดยไม่ต้อง busy wait หรือไม่ เช่น ลองคิดว่าการให้รัน consumer ภายใน time slice ของ producer จะเป็นไปได้ไหม
    อีกเรื่องที่เกี่ยวกัน ผมก็สงสัยว่า ถ้าทำให้มี operation FUTEX_WAKE ใน user space ได้ อาจลดต้นทุนการปลุก consumer ลงเหลือครึ่งหนึ่งของปกติ คือเหลือเฉพาะต้นทุนฝั่ง consumer เท่านั้นหรือไม่

  • เรียบร้อยดี และมีลิงก์ไปยัง paper ดี ๆ ด้วย
    แต่จะดีกว่าถ้าตัวเปรียบเทียบเป็น งานของ OpenMP เคยได้ยินชื่อเสียงมาว่า Rayon ค่อนข้างช้า

  • cooperative scheduling เป็นรากฐานของแพตเทิร์นจำนวนมากที่ให้ตัวชี้วัดยอดเยี่ยม

    • แต่ไม่ใช่ cooperative scheduling ในความหมายที่งานต่าง ๆ ยอม yield ให้กัน
      โดยหลักแล้วเป็นการร่วมมือเพื่อให้ส่งงานบางส่วนไปยัง thread อื่นได้ และไม่ได้ทำเสมอไปด้วย แต่เกิดแค่หนึ่งครั้งต่อ heartbeat เท่านั้น เนื่องจาก scheduling เกิดขึ้นไม่บ่อย ต้นทุนแบบ amortized จึงต่ำ
  • README ใต้ bench ก็น่าอ้างอิงเช่นกัน: https://github.com/judofyr/spice/blob/main/bench/README.md