2 คะแนน โดย GN⁺ 2024-10-03 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในสถานการณ์ที่มี การแข่งขันสูง ความแตกต่างของการติดตั้งใช้งาน Mutex จะเห็นได้ชัดเจน โดย pthread_mutex_t ของ Cosmopolitan Libc แสดงเวลาในการรันที่สั้นกว่าและการใช้ CPU ที่ต่ำกว่าการติดตั้งใช้งานหลักบน Windows และ Linux
  • ในการทดสอบบน Windows ด้วย 24-core Threadripper 29070WX Cosmopolitan เร็วกว่า Microsoft SRWLOCK 2.75 เท่า และใช้ทรัพยากร CPU น้อยกว่า 18 เท่า
  • บน Linux ด้วย 96-core Threadripper Pro 7995WX เร็วกว่า glibc 3 เท่า และเร็วกว่า musl libc 11 เท่า โดยช่องว่างของเวลา CPU ยิ่งกว้างกว่าเดิม
  • บน MacOS M2 Ultra นั้น Apple Libc นำหน้าเล็กน้อย และ Cosmopolitan ใช้อัลกอริทึมที่เรียบง่ายซึ่งพึ่งพา system call ulock ของ XNU บนสภาพแวดล้อม ARM
  • พื้นฐานของประสิทธิภาพนี้มาจากการผนวกรวม nsync ของ Google โดยมี CAS fast path, คิวผู้รอ, futex/ulock/WaitOnAddress(), การป้องกัน starvation และการออกแบบ designated waker เป็นหัวใจสำคัญ

วิธีทำเบนช์มาร์ก Mutex ภายใต้การแข่งขัน

  • การทดสอบจะสร้าง 30 เธรด และให้แต่ละเธรดเพิ่มค่าเลขจำนวนเต็มส่วนกลาง g_chores เดียวกัน 100,000 ครั้ง
  • การเพิ่มค่าแต่ละครั้งจะทำใน critical section ที่เล็กมาก ระหว่าง pthread_mutex_lock() และ pthread_mutex_unlock()
  • ค่าที่วัดใช้หน่วยไมโครวินาที และแยกเวลาออกเป็นสามแบบ
    • wall time: เวลาจริงที่โปรแกรมใช้รัน โดยรวมโอเวอร์เฮดของการสร้างเธรดและ join
    • user time: เวลา CPU ที่ใช้ใน user space
    • system time: เวลา CPU ที่ใช้ในเคอร์เนล
  • เนื่องจากหลายเธรดรันแบบขนาน ผลรวมของ user time และ system time จึงอาจมากกว่า wall time ได้
  • ในสถานการณ์ที่ไม่มีการแข่งขัน ความต่างด้านประสิทธิภาพระหว่างการติดตั้งใช้งานมักเล็ก แต่ใน สถานการณ์ที่มีการแข่งขัน ความต่างของการออกแบบ Mutex จะปรากฏชัด

Windows: Cosmopolitan เร็วกว่า SRWLOCK

  • การทดสอบบน Windows ทำบน 24-core Threadripper 29070WX
  • MutexShootout ของ Mark Waterman ประเมินว่า SRWLOCK ของ Windows เป็นการติดตั้งใช้งานที่แข็งแกร่งที่สุดในสถานการณ์ที่มีการแข่งขันสูง
  • ภายใต้เงื่อนไขเดียวกัน pthread_mutex_t ของ Cosmopolitan ทำ wall time ได้สั้นกว่า SRWLOCK และใช้ CPU ต่ำกว่า
การติดตั้งใช้งาน wall time user time system time
Cosmopolitan pthread_mutex_t 148,940µs 328,125µs 62,500µs
Microsoft SRWLOCK 410,416µs 5,515,625µs 1,640,625µs
Microsoft CRITICAL_SECTION 949,187µs 7,937,500µs 5,078,125µs
MSVC 2022 std::mutex 991,750µs 12,156,250µs 4,031,250µs
spin lock 1,165,435µs 24,515,000µs 15,000µs
Cygwin pthread_mutex_t 9,780,803µs 1,937,000µs 6,156,000µs
  • Cosmopolitan Mutex เร็วกว่า 2.75 เท่า เมื่อเทียบกับ Microsoft SRWLOCK และใช้ทรัพยากร CPU น้อยกว่า 18 เท่า
  • เมื่อเทียบกับ Cygwin Mutex ซึ่งให้การติดตั้งใช้งาน POSIX บน Windows แล้ว เร็วกว่า 65 เท่า
  • Cygwin Mutex ให้ผลลัพธ์ที่ช้ากว่า spin lock เสียอีกในกรณีใช้งานนี้

Linux: ช่องว่างของเวลา CPU มากกว่า wall time

  • การทดสอบบน Linux ทำบน 96-core Threadripper Pro 7995WX
การติดตั้งใช้งาน wall time user time system time
Cosmopolitan pthread_mutex_t 36,905µs 44,511µs 23,492µs
glibc pthread_mutex_t 101,353µs 150,706µs 2,724,851µs
spin lock 202,423µs 4,694,749µs 2,000µs
Musl libc pthread_mutex_t 411,013µs 2,167,898µs 9,926,850µs
  • Cosmopolitan Mutex เร็วกว่า glibc 3 เท่า และ เร็วกว่า musl libc 11 เท่า
  • หากดูจากเวลา CPU จะใช้ทรัพยากรน้อยกว่า 42 เท่า เมื่อเทียบกับ glibc และน้อยกว่า 178 เท่า เมื่อเทียบกับ musl libc
  • ในเวิร์กโหลดที่ทุกเธรดต้องทำงานแบบอนุกรม Cosmopolitan อาจดูใน htop เหมือนมีคอร์เดียวที่ทำงานอยู่
  • ในสถานการณ์เดียวกัน glibc และ musl libc อาจกินการใช้งาน CPU ได้สูงมาก ทำให้เป็นภาระมากขึ้นเมื่อรันหลายงานบนเซิร์ฟเวอร์เดียวกัน

MacOS: Apple Libc นำหน้าเล็กน้อย

  • การทดสอบบน MacOS ทำบน M2 Ultra
การติดตั้งใช้งาน wall time user time system time
Apple Libc 52,263µs 43,202µs 911,009µs
Cosmopolitan pthread_mutex_t 54,700µs 63,055µs 1,003,674µs
  • บน MacOS M2 ARM64 นั้น Apple Libc เร็วกว่า Cosmopolitan Mutex เล็กน้อย
  • การติดตั้งใช้งาน Mutex แบบทั่วไปของ Cosmopolitan ทำงานได้ไม่ดีบนแพลตฟอร์มนี้
  • บน MacOS ARM นั้น Cosmopolitan ใช้อัลกอริทึมที่เรียบง่ายกว่าซึ่งอิงจาก Futexes Are Tricky ของ Ulrich Drepper
  • วิธีนี้มอบงานหนักส่วนใหญ่ให้กับ system call ulock ของ XNU ทำให้ได้ประสิทธิภาพเกือบเทียบเท่าการติดตั้งใช้งานของ Apple

พื้นฐานของประสิทธิภาพ: การผนวกรวม nsync

  • หัวใจของประสิทธิภาพ Cosmopolitan Mutex คือการผนวกรวมไลบรารี nsync ของ Google
  • nsync เป็นไลบรารีที่มี GitHub star 371 ดวง และเขียนโดย Mike Burrows แห่ง Google
  • ระหว่างกระบวนการผนวกรวมเข้ากับ Cosmopolitan ได้มีการทำงานดังต่อไปนี้
    • ค้นพบและแก้บั๊กที่ไม่เคยถูกพบมานานในฟังก์ชัน unlock ของ Mutex ใน nsync
    • พอร์ตไปใช้ C11 atomic operation บน AARCH64 ทำให้ nsync Mutex ภายใต้การแข่งขัน เร็วกว่า upstream nsync 30%
    • เขียนการผนวกระดับระบบอย่าง futex ขึ้นใหม่เพื่อให้รองรับการพกพาของรันไทม์
    • ทำให้ทำงานร่วมกับการยกเลิกเธรดของ POSIX ได้อย่างราบรื่น

วิธีการทำงานของ nsync

  • nsync จะเริ่มจากลอง CAS(compare and swap) แบบมองโลกในแง่ดีทันที เพื่อให้ได้ล็อกอย่างรวดเร็ว
  • หากได้ล็อกไม่สำเร็จ จะเพิ่มเธรดผู้เรียกเข้าไปใน ลิสต์เชื่อมโยงสองทาง ของผู้รอ
    • ผู้รอแต่ละรายมี semaphore ของตัวเองอยู่บน cache line แยกอิสระ
    • เมื่อเธรดเข้าสู่สถานะรอแล้ว จะไม่แตะต้องล็อกหลักอีก
    • สิ่งนี้สำคัญต่อการลดโอเวอร์เฮดการสื่อสารที่เกิดขึ้นเมื่อหลายคอร์แตะ cache line เดียวกัน
    • พื้นหลังที่เกี่ยวข้องเชื่อมไปยัง What Every Programmer Should Know About Memory ของ Ulrich Drepper
  • nsync ใช้ futex ของระบบปฏิบัติการเพื่อทำให้เธรดหลับ
    • บน MacOS futex ถูกเรียกว่า ulock
    • บน Windows WaitOnAddress() ทำหน้าที่แบบเดียวกับ futex
    • ในบรรดาระบบปฏิบัติการที่ Cosmo รองรับ มีเพียง NetBSD ที่ไม่มี futex โดยใช้ POSIX semaphore ที่ติดตั้งใน kernel space และต้องใช้ file descriptor ใหม่สำหรับแต่ละ semaphore
  • nsync หลีกเลี่ยง starvation ด้วยแนวคิด “long wait”
    • หากผู้รอถูกปลุก 30 ครั้งแต่ล้มเหลวในการได้ล็อกทุกครั้งภายใน ก็จะเพิ่มบิตบนล็อกเพื่อไม่ให้เธรดที่ยังไม่เคยรอได้ล็อกก่อน
    • เมื่อมีบิตนี้อยู่ CAS เริ่มต้นของเธรดที่เพิ่งเข้ามาใหม่จะล้มเหลวจนกว่าคิวรอจะถูกระบายออกไปในระดับหนึ่ง
  • กรณีใช้งานที่มีการแข่งขันบน critical section ขนาดเล็กจะเร็วขึ้นด้วยแนวคิด designated waker
    • เมื่อมีเธรดบางตัวตื่นขึ้นเพื่อพยายามได้ล็อก จะมีการตั้งค่าบิตบนล็อกหลัก
    • ใน nsync ฟังก์ชัน unlock เป็นผู้รับผิดชอบในการปลุกเธรดผู้รอถัดไป
    • บิตนี้ช่วยให้เธรดที่กำลัง unlock ไม่จำเป็นต้องปลุกผู้รอคนที่สอง หากมีเธรดที่ถูกปลุกแล้วอยู่ก่อนหน้า
  • ซอร์สโค้ดที่เกี่ยวข้องอยู่ใน cosmopolitan/third_party/nsync/mu.c และ cosmopolitan/libc/intrin/pthread_mutex_lock.c

บริการจริงและโค้ดสำหรับตรวจสอบ

  • เดโมสดที่ใช้ Cosmo Mutex สามารถดูได้ที่เซิร์ฟเวอร์ http://ipv4.games/
  • บริการนี้รันอยู่บน GCE VM 2 คอร์ และจนถึงตอนนี้เคยทนต่อบอตเน็ต DDoS ขนาดสูงสุด 49,131,669 IP มาแล้ว
  • ด้วย nsync จึงสามารถย้าย SQL query ไปยังเธรดเบื้องหลัง และใช้โครงสร้างที่เธรดส่งข้อความหากันได้
  • สามารถดูตัวชี้วัดสถานะได้ที่ /statusz
  • โค้ดเบนช์มาร์กวัด wall time ด้วย gettimeofday() และวัด user time กับ system time ด้วย getrusage()
  • ตอนท้ายจะตรวจสอบว่า g_chores == THREADS * ITERATIONS เพื่อยืนยันว่ามีการทำงานเพิ่มค่าครบทั้งหมด

ข้อควรระวังเมื่อดู spin lock

  • ในสถานการณ์ที่ไม่มีการแข่งขัน ความต่างระหว่างการติดตั้งใช้งาน Mutex มีน้อย และ spin lock ที่เขียนไม่กี่บรรทัดอาจดีกว่าก็ได้
  • แต่ควรใช้ spin lock เฉพาะเมื่อไม่มีทางเลือกอื่นจริง ๆ เท่านั้น
  • มันมีประโยชน์ในที่อย่างเคอร์เนล ซึ่งมีข้อจำกัดระดับต่ำมากจนใช้วิธีที่ซับซ้อนกว่านี้ได้ยาก
  • spin lock ยังอาจถูกใช้เป็นรายละเอียดการติดตั้งใช้งานภายในของ nsync lock ได้ด้วย
  • หากดูประสิทธิภาพของ lock จาก wall time เพียงอย่างเดียว spin lock อาจดูดี ดังนั้นควรตรวจดู เวลา CPU ร่วมกันด้วยผ่าน getrusage()

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

 
GN⁺ 2024-10-03
ความคิดเห็นจาก Hacker News
  • การได้เห็นการนำ mutex แบบใหม่มาเทียบกันนั้นน่าสนใจเสมอ แต่วิธีทำเบนช์มาร์กนี้ไม่ค่อยถูกใจ ดูแทบจะเป็น ไมโครเบนช์มาร์ก
    คนที่นำ lock เร็ว ๆ ไปใช้จริงมักใช้โปรแกรมมัลติเธรดขนาดใหญ่มากเป็นเครื่องมือทดสอบประสิทธิภาพหลัก ในเวิร์กโหลดที่ซับซ้อน ซึ่งความยาวของ critical section จำนวนเธรดที่แย่งกัน และระดับการแข่งขันเปลี่ยนแปลงหลากหลาย ปัจจัยที่ทำให้ mutex เร็วหรือช้าดูเหมือนจะต่างกันไป
    เผื่อเป็นข้อมูล ผมเป็นคนเขียน lock เร็วของ WebKit คิดค้น ParkingLot abstraction สำหรับการทำ lock (ใช้ใน Rust และ Unreal Engine ด้วย) และเคยทำงานวิจัยกับ paper เกี่ยวกับ lock เร็วสำหรับ Java มาก่อน

    • ขอเสริมในฐานะคนทำแอปเดสก์ท็อป ผมอยากเห็นตัวเลขประสิทธิภาพในกรณีที่ การแข่งขันไม่รุนแรง สำหรับแอปที่มีเธรดหลายสิบตัวหมุนทำงานบ่อย ๆ
      ในฐานะโปรแกรมเมอร์เสียงแบบเรียลไทม์ ค่าใช้จ่ายในการจับ mutex ที่ยังไม่ได้ถูกล็อกอยู่สำคัญกว่า ในแอปของเรา สถานการณ์นี้เกิดขึ้นมากที่สุดอย่างท่วมท้น ในทำนองเดียวกัน ผมอยากรู้ค่าใช้จ่ายของ try-lock operation ที่จะล้มเหลวด้วย ไม่ใช่ตอนที่มีเธรด N ตัวมาแย่งกัน
      Cosmopolitan เป็นโอเพนซอร์ส ดังนั้นจะวัดเองก็ได้ แต่ก็ยังรู้สึกเสียดาย
    • ผมก็คิดเหมือนกัน mutex มีหลายชนิด และบางอย่างเหมาะกับเวิร์กโหลดเฉพาะมากกว่า นึกถึง DistributedMutex กับ SharedMutex (https://github.com/facebook/folly/blob/main/folly/synchroniz..., https://github.com/facebook/folly/blob/main/folly/SharedMute...)
      เหมือนกับ hashmap นั่นแหละ แทบไม่มี hashmap เดียวที่ดีกว่าสำหรับทุกเวิร์กโหลดที่เป็นไปได้
    • mutex สไตล์นี้จะถูกใช้ใน PyMutex ของ Python 3.13 ด้วย มีเบนช์มาร์กจริงที่แสดงว่า PyMutex เร็วกว่า PyThread_type_lock ก่อนเวอร์ชัน 3.13 แค่ไหน
    • แน่นอนว่าเป็นไมโครเบนช์มาร์ก และมีโอกาสสูงที่จะไม่ได้เป็นตัวแทนของประสิทธิภาพทั่วไป หน้านี้ให้มาตรฐานที่ดีของแนวปฏิบัติในการเบนช์มาร์กระบบปฏิบัติการ เพียงแต่จะเอียงไปทางสายวิชาการมากกว่า: https://gernot-heiser.org/benchmarking-crimes.html
    • เบนช์มาร์กเฉพาะตัวนั้นอาจถึงขั้นเอื้อให้พฤติกรรมที่ไม่พึงประสงค์ เช่น ความไม่ยุติธรรมแบบพยาธิสภาพ การ schedule ที่เหมาะที่สุดคือรัน operation เพิ่มค่าของเธรดแรกทั้งหมด แล้วค่อยรันของเธรดที่สองทั้งหมดต่อไป เพราะทำแบบนี้จะลดทราฟฟิกข้ามโปรเซสเซอร์ให้ต่ำที่สุด
      mutex ที่เมื่อได้ lock ไม่สำเร็จแล้วหลับเป็นเวลาคงที่ (เช่น 100µs) แทบจะทำให้งานถูกรวมเป็นก้อนเสมอ จึงเข้าใกล้พฤติกรรมแบบนี้และอาจ “ชนะ” ในเบนช์มาร์กได้ แต่ในแอปพลิเคชันจริง ถ้ามีการแข่งขันแม้เพียงเล็กน้อย mutex แบบนั้นจะแย่มาก
      ไม่ได้หมายความว่า mutex นี้แย่ หรือ pthread mutex ดี แต่หมายความว่าไมโครเบนช์มาร์กนั้นไม่ได้วัดสิ่งที่น่าจะทำนายประสิทธิภาพของแอปพลิเคชันจริงได้
  • ตรงส่วนที่ว่า “เหตุผลที่ Cosmopolitan Mutex ดีคือเพราะใช้ไลบรารี nsync” ผมเพิ่งเคยได้ยินชื่อ nsync เป็นครั้งแรก แต่ Mike Burrows ก็เป็นคนเขียน implementation ของ mutex ที่ใช้ใน production ของ Google ด้วย: https://github.com/abseil/abseil-cpp/blob/master/absl/synchr...
    เลยสงสัยว่าทำไม implementation ของ mutex ตัวนี้ถึงไม่อยู่ในเบนช์มาร์ก และถ้าบน macOS มอบหมายให้ __ulock ทำงาน การใช้แค่ member functions wait(), notify_one() ใน atomic library ของ libc++ ก็น่าจะทำให้ได้ผลแบบเดียวกันอย่างเรียบง่ายกว่า
    เคยมีเธรดใหญ่เกี่ยวกับการปรับปรุง implementation ของ mutex ใน Rust ด้วย: https://github.com/rust-lang/rust/issues/93740#issuecomment-... จุดที่น่าสนใจคือมีการคุยรายละเอียดการทำงานภายในของ implementation mutex ยอดนิยมแทบทั้งหมด

    • ตอนผมเข้า AV นั้น Mike ก็เป็นตำนานไปแล้ว มีเรื่องเล่าว่าเมื่อใดก็ตามที่ search engine ต้องเร็วขึ้น เขาจะเข้ามาเขียนฟังก์ชันหลัก ๆ ใหม่สักสองสามตัว แล้วกลับไปทำงานเดิมของเขา
      อาจเป็นเรื่องจริง แต่ผมยืนยันเองไม่ได้ เขาเป็นวิศวกรที่ฉลาดสุด ๆ และให้ความสำคัญกับประสิทธิภาพมาก เพียงแต่เราไม่ได้รันอะไรบนเซิร์ฟเวอร์เครื่องเดียวนาน ๆ
    • Burrows ยังมีส่วนเกี่ยวข้องกับ Burrows-Wheeler transform, Bigtable, Dapper, Chubby และอื่น ๆ ด้วย
    • เธรด Rust นั้นสุดท้ายก็ไปถึงเรื่องนั้น แต่โดยพื้นฐานแล้วเป็นเรื่องงานของ Mara และจึงมีการกล่าวถึงหนังสือของเธอที่ออกเมื่อเดือนมกราคม 2023 ด้วย
      implementation ของ mutex ใน Rust ปัจจุบันเพิ่งถูกนำเข้าเมื่อต้นปีนี้ และแม้บน Linux อาจไม่ได้ต่างกันมาก แต่บน Windows กับ Mac ผมเข้าใจว่าเป็นงานใหม่
      ถึงอย่างนั้น สิ่งที่ Mara อธิบายเกี่ยวกับภายในของ implementation อื่น ๆ ก็ยังน่าสนใจอยู่ แต่ควรตรวจดูด้วยว่าข้อมูลนั้นล้าสมัยสำหรับสถานการณ์ของตัวเองหรือไม่
    • เหตุผลที่ implementation ของ mutex ใน Abseil ไม่อยู่ในเบนช์มาร์กอาจเป็นเพราะมันเป็น implementation ภาษา C++ ไม่ใช่ C แค่เดานะ
    • Mike Burrows ดูเหมือนจะเคยได้รับรางวัล ACM ด้วย และมีรูปของเขาอยู่ในนั้น
      https://awards.acm.org/award-recipients/burrows_9434147
  • ประโยคที่ว่า “ถึงจะยังเป็นไลบรารี C ใหม่ที่ยังมีจุดหยาบอยู่บ้าง แต่มันดีขึ้นเร็วมากจนการไม่ใช้ในโปรดักชันเริ่มดูเหมือนเป็นการละเลยความรับผิดชอบทางวิชาชีพ” ฟังดูค่อนข้างแปลก แม้จะประเมิน โปรเจกต์ Cosmopolitan ไว้สูง แต่คำกล่าวอ้างเรื่องความเหนือกว่าแบบเกินจริงเช่นนี้มักเป็นสัญญาณเตือนที่ค่อนข้างไม่ดี

    • ผมคิดว่าข้อกล่าวอ้างของ Justine โดยรวมแล้วค่อนข้างถูก เพียงแต่การใช้ถ้อยคำที่เกินจริงและดูอวดตัวน่าจะเป็นสไตล์ หรือไม่ก็บุคลิกของเขา
      เข้าใจได้ว่าบางคนอาจมองว่ามันแรงเกินไป ก่อนหน้านี้ก็เคยเกิดดราม่าในลักษณะนั้นกับ llamacpp มาแล้ว
    • Justine ดูเหมือนเป็นคนที่เก่งและสร้างสรรค์มาก แต่ผมไม่อยากใช้ libc ที่ “ใหม่” และ “ยังมีจุดหยาบ” ในโปรดักชัน
      สิ่งที่สำคัญที่สุดในโปรดักชันไม่ใช่ “ดีขึ้นเร็วอย่างเหลือเชื่อ” แต่คือ เสถียรภาพ ความคาดการณ์ได้ และความน่าเชื่อถือ แน่นอนว่าประสิทธิภาพก็สำคัญ โค้ดที่เร็วขึ้นอาจช่วยลดอินฟราสตรักเจอร์ ลดต้นทุน และดีต่อสิ่งแวดล้อมได้ แต่ความเร็วอยู่ลำดับท้ายสุด
    • เมื่ออยู่คนเดียวหน้าเครื่องคอมพิวเตอร์เขียนโค้ดเป็นเวลานาน บางทีการขาดปฏิสัมพันธ์ทางสังคมก็อาจทำให้เกิด ความหยิ่งผยอง ขึ้นมาบ้าง หากไม่มีสิ่งคอยถ่วงดุลความสำคัญของตัวเองหรือผลงานของตัวเอง แม้ผลงานจะน่าประทับใจ แต่ก็อาจดูยิ่งใหญ่เกินกว่าระดับการยอมรับในวงกว้างไปได้
      ตัวอย่างเช่น APE รู้สึกว่าเป็นแฮ็กที่น่าประทับใจมาก แต่ก็สามารถวิจารณ์ได้ว่า “นี่หมายความว่าตอนนี้มันไม่ได้แค่ไม่ปลอดภัยบนแพลตฟอร์มเดียว แต่สามารถไม่ปลอดภัยพร้อมกันได้หลายแพลตฟอร์มแล้วหรือ?”
      ยิ่งอยู่ในวงการเทคโนโลยีนานขึ้น ก็ยิ่งตระหนักว่าผลประโยชน์ร่วมแบบได้กันถ้วนหน้าอย่างสมบูรณ์นั้นหายากมาก และส่วนใหญ่ล้วนเป็น trade-off ที่มีทั้งสิ่งที่ได้และสิ่งที่เสีย
    • อย่างน้อยสำหรับผม มันดูเหมือนมุกตลก
    • สงสัยว่าคุณเคยคิดไหมว่า sense of humor ของคุณกับ Justine อาจต่างกัน และก็ไม่รู้เหมือนกันว่าการเอาเรื่องนี้มาโพสต์ที่นี่จะช่วยใครได้อย่างไร
  • ออกนอกประเด็นไปไกลเลย แต่ในฐานะนักพัฒนาเกม ผมกลับชอบ mutex ที่ช้า ซึ่งทำงานดีบักเยอะ ๆ ในทุก developer build เช่น มีชื่อ/ID สำหรับดีบัก ติดตาม owner รายงานเวลาที่ใช้ไปกับ contention ไปยัง profiler และรายงานการเปลี่ยน ownership ไปยัง profiler ด้วย
    เกมมักจัดโครงสร้าง concurrency แตกต่างออกไป และก็มีการพัฒนาแพตเทิร์นเพื่อหลีกเลี่ยง lock แต่แพตเทิร์นเหล่านั้นใช้งานยาก และโปรแกรมเมอร์ต้องเปลี่ยนโครงสร้างโค้ด โค้ดส่วนใหญ่เริ่มจาก “ใส่ lock ตรงนี้ไปก่อน แล้วให้ผ่าน milestone ให้ได้”
    lock ที่เร็วก็อาจช้าลงอย่างคาดเดาไม่ได้ และถ้าเคยมี real-time guarantee ก็จะทำให้พังได้ โดยเฉลี่ยอาจเร็ว แต่ tail latency ก็ไม่ได้หายไป ผมไม่อยากเป็นคนที่ต้องกลับมาตามหาว่า “ทำไมเกมเรากระตุก” แต่โดยปกติแล้วผมก็มักกลายเป็นคนนั้น
    ดังนั้นใช้ lock ที่ช้าดีกว่า แบบที่เห็นเป็นสีแดงก้อนใหญ่ใน profiler นั่นแหละ ถ้าเห็นว่ามันโดนหนักอยู่ ก็ refactor เพื่อลบมันออกได้
    รู้ว่านี่เป็นข้อเรียกร้องที่ยาก ในโปรดักชันระดับ AAA คนที่ใช้ profiler เป็นมีนับนิ้วได้ ต่อให้ดูมาหลายโปรดักชันก็เป็นแบบนั้นเสมอ
    ขอโทษที่บ่น แต่ก็หวังว่างานวิจัยเรื่อง primitive และอัลกอริทึม concurrency ที่เร็วจะเดินหน้าต่อไป

    • ออกนอกประเด็นไปอีกนิด นี่เป็นหนึ่งในเหตุผลที่ทำให้การพัฒนาเกมด้วย Rust สนุก
      ในเกม ถ้าเป็นไปได้ เราไม่อยากมี lock contention เลย และในหลายกรณีก็พิสูจน์ได้ว่าการจับ lock นั้นไม่จำเป็น ตัวอย่างเช่น แต่ละเฟรมถูกแบ่งเป็นหลาย phase และการเข้าถึงแบบ mutable ไปยัง shared resource บางอย่างจำเป็นเฉพาะใน phase หนึ่งเท่านั้น เช่น update() ก่อน render() หรือการ hot reload แอสเซ็ต
      เมื่อใช้ scoped thread กับกฎการยืมของ Rust เราสามารถจัดโครงสร้างให้ไม่ต้องใช้ mutex เลย และมั่นใจได้ว่าหากภายหลังโค้ดเปลี่ยนจนจำเป็นต้องใช้จริง ๆ compiler จะรายงาน error อย่างเข้มงวดทันที
      ถ้าเลือกได้ การได้ compile error ย่อมดีกว่า spike ใน profiler เสมอ
    • เห็นด้วยอย่างยิ่ง ฟีเจอร์ดีบัก อย่างการตรวจจับ deadlock หรือการตรวจสอบสถานะภายในนั้นคุ้มค่าตัวได้ง่าย ๆ ถ้าคุณ acquire lock บ่อยจนกระทบ performance ก็ควรกลับไปดู design ใหม่ ควรหลีกเลี่ยงการแชร์ mutable state ระหว่าง thread
  • ในอีกด้านหนึ่ง กลุ่ม Cosmo/APE/redbean ดูยอดเยี่ยมจริง ๆ และคอมเมนต์ในบทความที่เกี่ยวข้องก็ส่วนใหญ่เป็นบวก แทบไม่มีเนื้อหาที่โต้แย้งแนวคิดเองด้วย แต่ในอีกด้านหนึ่ง ผมแทบไม่เคยได้ยินว่ามีคนอื่นใช้สิ่งนี้อยู่เลย
    แน่นอนว่าไม่ใช่ทุกคนจะแชร์งานของตัวเองอย่างกว้างขวาง แต่ถ้าผ่านมาหลายปีแล้วก็น่าจะได้เห็นบทความ retrospective ของโปรเจกต์สักสองสามชิ้นบ้าง การกล่าวถึง Cosmo/APE/redbean ที่ผมเห็นทั้งหมดมาจากไซต์ของ Justine ทั้งนั้น
    เลยสงสัยว่า มีหลุมพรางที่ซ่อนอยู่หรือเปล่า? เป็นเครื่องมือที่ทำอะไรไม่ดีบางอย่างเพื่อให้ได้ผลลัพธ์หรือเปล่า? เป็นมุกหรือการ troll แบบ tom7 ที่ผมไม่เข้าใจเพราะไม่รู้ลึกเรื่อง compiler หรือ runtime หรือเปล่า? หรือจริง ๆ แล้วเป็นเครื่องมือสุดบรรเจิดที่ยังไม่แพร่หลายเท่านั้น?

    • APE ทำงานด้วย ทริกที่ฉลาดแกมโกง ซึ่งอาจถูกปิดกั้นได้ทุกเมื่อ และบน OpenBSD ก็ถูกปิดกั้นไปแล้วจริง ๆ
      คนส่วนใหญ่ที่ทำซอฟต์แวร์ข้ามแพลตฟอร์มไม่ได้ต้องการไฟล์ executable เดียวที่รันได้บนทุกแพลตฟอร์ม แต่ต้องการ codebase เดียวที่ทำงานได้ถูกต้องบนแต่ละแพลตฟอร์มที่รองรับ
      จากมุมมองนั้น ภาษาอย่าง Go ที่หากเลี่ยง CGO ได้ก็ cross-compile ไปยังทุก target ได้ จึงเป็นสิ่งที่น่าใช้ แต่เวทมนตร์ของ APE ที่รันได้สามรูปแบบ แม้จะฉลาดมาก ก็ไม่ได้ทำให้มั่นใจว่าจะทำงานได้ตลอดไป และสำหรับคนส่วนใหญ่ก็แทบไม่มีประโยชน์เชิงปฏิบัติมากนัก
      แต่ละแพลตฟอร์มมีข้อกำหนดด้าน packaging และ signing ของตัวเอง ดังนั้น compile แยกเป็น target รายแพลตฟอร์มจะดีกว่า
    • โดยส่วนตัวแล้ว cosmo และ ape ดูฉลาดมาก แต่ถ้าเครื่องมือธรรมดา ๆ ทำงานได้ดีอยู่แล้ว งานจริงก็ไม่ต้องการความฉลาดแบบนี้
      ตัวอย่างเช่น ถ้าคุณสามารถ cross-compile โปรเจกต์ไปยังระบบปฏิบัติการและแพลตฟอร์มอื่น ๆ ได้อยู่แล้ว หรือมี build infrastructure แบบนั้นอยู่แล้ว ก็ไม่มีเหตุผลต้องหาโซลูชันที่สร้าง binary เดียวที่ทำงานได้ทุกที่
      อีกอย่าง APE ใช้การแฮ็กที่ฉลาดเพื่อให้รันได้บนหลายระบบปฏิบัติการ แล้วถ้าสักวันหนึ่งการแฮ็กนั้นพังเพราะรูปแบบไฟล์ executable พัฒนาไปล่ะ? ถ้าไม่มีใครมีเวลาปรับ APE ให้รองรับความเปลี่ยนแปลงนั้นล่ะ?
      ในทางกลับกัน เครื่องมือที่น่าเบื่อ อย่าง gcc, clang, go, rust จะยังคงได้รับการอัปเดต และยังทำงานต่อไปได้บนระบบปฏิบัติการที่วิวัฒนาการอยู่เรื่อย ๆ ดังนั้นผมจึงเลือกอยู่กับฝั่งที่น่าเบื่อ เหตุผลที่ไม่สนใจของฉลาด ๆ ก็เพราะของน่าเบื่อมันทำงานได้ดีสำหรับผมอยู่แล้ว
    • llamafile ของ Mozilla ใช้สิ่งนี้ โดยรวม model weights กับไฟล์ executable ไว้เป็นหนึ่งเดียว เพื่อให้รันได้ทุกที่บนแพลตฟอร์ม cosmo/ape และยังเปิด redbean HTTP server สำหรับการโต้ตอบให้ด้วย
      จะรันโดยไม่รวม weights เข้าไป แล้วให้อ่าน weights จาก filesystem ก็ได้ นี่อาจเป็นวิธีที่ง่ายที่สุดในการ “ดาวน์โหลดแล้วรันได้ทันที” สำหรับ local LLM
    • Cosmopolitan ให้ความรู้สึกเหมือนช่องโหว่ทางเทคนิคที่เอามาเขียนบล็อกสนุก ๆ ได้เสมอ เป็นประเภทที่แทบการันตีว่าจะขึ้นหน้าแรกของที่อย่าง HN ได้ด้วยความบรรเจิดและความหมกมุ่นกับการตั้งค่า
      แต่ถ้าจะใช้เป็นเทคโนโลยีพื้นฐานอย่าง libc ก็ดูเหมือนจะมีประโยชน์หลัก ๆ ในฐานะของเล่นสนุก ๆ หรือโปรเจกต์ส่วนตัวขนาดเล็กมากกว่า
      ในบริบทนั้น เวลามันถูกนำเสนอเป็นทางเลือกจริงจังแทนสิ่งอย่าง glibc, musl, msvcrt จึงรู้สึกแปลกนิดหน่อย เป็นการแฮ็กที่น่ารักมาก แต่ถ้าผมพบว่ามีสิ่งที่ผมพึ่งพาอย่างจริงจังใช้อยู่ ก็คงค่อนข้างตกใจ
    • Mozilla มีโปรเจกต์ Llamafile ที่อิง Cosmopolitan libc: https://github.com/Mozilla-Ocho/llamafile
      บน Hugging Face ก็มีการอัปโหลดโมเดลยอดนิยมที่ repackaging เป็นรูปแบบนั้นอยู่เป็นประจำ: https://huggingface.co/models?search=llamafile
      อย่างไรก็ตาม มันจะมีประโยชน์ใช้งานจริงมากไปกว่าการลองโมเดลขนาดเล็กอย่างรวดเร็วหรือไม่ ก็เป็นอีกประเด็นหนึ่ง
  • ถ้ามันดีขนาดนั้น ก็สงสัยว่าทำไมไลบรารี C ทุกตัวถึงไม่เอาทริกเดียวกันไปใช้
    เดาว่าทริกพวกนั้นอาจเร็วเสมอเฉพาะบนสถาปัตยกรรมบางแบบ, CPU บางรุ่น, เวิร์กโหลดหรือแพตเทิร์นการเข้าถึงบางอย่างเท่านั้น ถ้าเบนช์มาร์กเวิร์กโหลดหลากหลายบนฮาร์ดแวร์ทั้งหมดที่รองรับอย่างถูกต้อง อาจไม่ได้ข้อดีแบบเดียวกันก็ได้
    หรือไม่เช่นนั้น ความหมายเชิงสัญญา (semantics) ของ pthread API ที่ Cosmopolitan พยายาม implement อาจแตกต่างกันอย่างละเอียด และ implementation นี้อาจไม่ได้ปฏิบัติตามสเปกอย่างเคร่งครัด
    นึกภาพยากว่าผู้เขียน libc หลายรายจะตามงานวิจัยล่าสุดของ primitive ระดับระบบปฏิบัติการไม่ทัน

    • โปรเจกต์แบบนั้นมีลำดับความสำคัญเป็นสิบ ๆ อย่าง ไม่ใช่แค่ API ตัวใดตัวหนึ่ง การหมกมุ่นกับ API รายตัวไม่ใช่วิธีใช้เวลาที่มีจำกัดได้ดีนัก และถ้าจะยกตัวอย่างโต้แย้ง ก็ดู malloc กับรูทีนสตริง ใน libc ทั่วไปของ Linux ได้เลย
      malloc ของ glibc ก็พอใช้ได้ แต่ในด้านความเร็วและความสามารถในการขยายโดยรวม แพ้ทางเลือกที่ทันสมัยกว่าได้ง่าย มี fragmentation หนักและแย่ลงเมื่อเวลาผ่านไป อีกทั้งยังมีค่าปรับแต่งอย่าง MALLOC_ARENA_MAX จำนวนมากที่ส่งผลใหญ่ต่อเวิร์กโหลดจริง ส่วน musl malloc นั้นแย่มากในแง่ประสิทธิภาพทุกระดับ การใช้ allocator ของ musl ในโปรแกรมหลายเธรดเคยทำให้ประสิทธิภาพพังหนักจนแทบเรียกได้ว่าเป็นความประมาท
      musl ยังไม่มีของอย่างรูทีนเปรียบเทียบสตริงที่ปรับแต่งด้วย SIMD ด้วย ถ้ารู้ว่า CPU cycles ถูกใช้ไปกับงานพวกนี้มากแค่ไหนในโปรแกรมที่ไม่เล็กน้อย คุณจะประหลาดใจ และมันก็เห็นชัดในโปรไฟล์จริง การปรับปรุงส่วนนี้ทำให้แทบทุกโปรแกรมดีขึ้นโดยทั่วไป รูทีนที่ปรับแต่งของ glibc นั้นดี แต่ก็ดูเหมือนว่ายังเร็วขึ้นได้อีก
      สิ่งเหล่านี้ไม่ใช่ “การปรับแต่งที่เฉพาะกับสถาปัตยกรรมเดียวจนทั่วไปไม่ได้” โดยเฉพาะสองด้านนี้เป็นพื้นที่ที่มีการสำรวจและเข้าใจกันดี ซึ่งลดเวลาแบบ wall-clock ได้ 2~5 เท่าในแทบทุกเวิร์กโหลด และยังปรับปรุงการใช้ working set ระยะยาวได้มาก แล้วทำไมถึงไม่ถูกนำไปใช้? เป็นไปได้ว่าสาเหตุเหมือนเคย คือมีอย่างอื่นต้องทำ หรือมีลำดับความสำคัญที่ขัดกัน เช่น musl ที่ให้ความสำคัญกับความเรียบง่ายมากกว่าประสิทธิภาพสูงสุด
      ไม่ได้จะโทษโปรเจกต์เหล่านี้ ไม่มีใครพูดว่า “โปรแกรมของฉันช้าแย่มาก ถูกออกแบบมาให้ทำอะไรไม่ดีสักอย่าง และฉันภูมิใจกับมัน” เพียงแต่ความคิดที่ว่าคนทำโปรเจกต์เหล่านั้นเลือกออกแบบเฉพาะ Pareto frontier ที่สมบูรณ์แบบนั้นไม่สมจริงเลย และไม่สะท้อนวิธีที่โปรเจกต์ส่วนใหญ่ดำเนินไปจริง ๆ
    • อาจเป็นเรื่องการเมือง, อาการ NIH syndrome, หรือ maintainer รุ่นเก่าก็ได้
      การจะเปลี่ยนอะไรใน glibc หรือสิ่งเทียบเท่าฝั่ง C++ ใช้เวลานานเหมือนตลอดกาล
      synchronization primitive มีหลายชนิด แต่ pthreads รองรับแค่บางส่วน ถ้าจำกัดตัวเองไว้แค่นั้น โดยมากก็เท่ากับยอมเสียประสิทธิภาพเพื่อแลกกับ portability
    • สงสัยว่าประโยค “นึกภาพยากว่าผู้เขียน libc หลายรายจะตามงานวิจัยล่าสุดของ primitive ระดับระบบปฏิบัติการไม่ทัน” เป็นการประชดหรือเปล่า
      ผมไม่รู้เรื่อง maintainer ของ libc แต่ในฐานะคนที่ maintain บางอย่างอยู่ ผมไม่ได้พยายาม implement งานวิจัยล่าสุด สิ่งที่ทำคือรักษาเสถียรภาพและตรวจให้แน่ใจว่าประสิทธิภาพยอมรับได้ การนำงานวิจัยมา implement อยู่นอกงบ “การบำรุงรักษา” ของผม
    • สงสัยว่าการเปลี่ยน implementation ของ pthread mutex มี ข้อพิจารณาเรื่อง ABI หรือเปล่า
    • คำถามว่า “ถ้ามันดีขนาดนั้น ทำไมไลบรารี C ทุกตัวถึงไม่เอาทริกเดียวกันไปใช้?” ทำให้นึกถึงมุกนี้
      ชายคนหนึ่งกับนักสถิติกำลังเดินไปตามถนนแล้วเห็นธนบัตร 50 ยูโร นักสถิติเดินต่อไป ส่วนชายคนนั้นหยุดแล้วพูดว่า “ดูสิ มีเงินอยู่บนพื้น” นักสถิติจึงตอบว่า “คงเป็นของปลอมแหละ ถ้าเป็นของจริงคงมีคนเก็บไปแล้ว” แล้วก็เดินต่อไป ชายอีกคนจึงหยิบเงินนั้นไป
  • เธรดและ mutex เป็นองค์ประกอบที่ทำให้วิทยาการคอมพิวเตอร์ซับซ้อนที่สุดอย่างหนึ่ง implementation ใหม่ ๆ ควรถูกมองอย่างกังขาเสมอ จนกว่าจะถูกใช้งานในวงกว้างเป็นเวลาหลายปี
    บั๊กในกลไก threading แบบนี้มักหลุดรอดแม้แต่การตรวจทานที่เข้มข้นที่สุด ตอน Java โผล่มาช่วงกลางยุค 90 ก็เผยบั๊กสารพัดเกี่ยวกับเธรดและ mutex บน Solaris
    สิ่งที่ต้องการไม่ใช่ implementation ของ mutex ที่เร็วที่สุด แต่คือ implementation ที่เชื่อถือได้

    • mutex ยังห่างไกลจากการเป็นสิ่งที่ “ซับซ้อน” ที่สุด วิธี implement ให้มีประสิทธิภาพก็ไม่ได้มีมากนัก ส่วนใหญ่แล้ว โดยเฉพาะใน read path ทางที่ดีที่สุดคือหลีกเลี่ยงมัน
  • โค้ดนี้ไม่ได้เบนช์มาร์กประสิทธิภาพของการล็อก mutex แต่เบนช์มาร์ก การแข่งขันแย่ง mutex ถ้าคุณใช้ล็อกแบบนี้อยู่ ก็ควรประเมินโค้ดใหม่
    ทุกครั้งที่แต่ละเธรดเพิ่มค่า g_chores ก็จะล็อกและปลดล็อก mutex ทำให้เกิด overhead จากการ acquire และ release mutex บ่อย ๆ โดยทำซ้ำ 100,000 ครั้งต่อเธรด
    overhead นี้บดบังความแตกต่างด้านประสิทธิภาพจริงระหว่างกลไกล็อก เพราะเบนช์มาร์กถูกครอบงำด้วยการแข่งขันแย่งล็อก ไม่ใช่งานจริง เบนช์มาร์กแบบนี้ไม่มีประโยชน์

  • เป็นแฟนของ Justine และผลงานของเธอ แต่กรณีนี้น่าจะเป็น test case สำหรับเบนช์มาร์ก mutex ที่น่าสนใจน้อยที่สุดแล้ว สถานการณ์ที่หลายเธรดคอยทุบ mutex ตัวเดียวกันไม่หยุด เป็นสิ่งที่ควรหลีกเลี่ยงตั้งแต่แรก
    ดังนั้น implementation ของ mutex ตัวไหนจัดการกรณีนี้ได้ดีที่สุดจึงไม่ค่อยน่าสนใจนักในมุมมองของผม

    • สงสัยว่าคุณคิดว่า test case ที่ดีสำหรับการเบนช์มาร์ก mutex คืออะไร
    • กรณีส่วนใหญ่ที่ผมใช้ล็อกหรือ semaphore คือรอบ ๆ resource ที่แพงมาก ๆ การใช้ resource นั้นกลบ overhead ด้านประสิทธิภาพของล็อกไปหมด
    • แล้วควรวัดอะไร? กรณีที่ไม่มีการแข่งขันนั้นสำคัญและเป็น baseline แต่ถัดจากนั้น จุดอ่อนของ mutex ก็คือจุดนี้เอง ถ้าจัดการการแข่งขันได้ไม่ดี ฮาร์ดแวร์ก็จะว่างงาน งานของ scheduler จะเพิ่มขึ้น หรือมีการเข้า kernel มากขึ้น
      มีเรื่องสำคัญอีกอย่างที่ขาดไป คือในสถานการณ์ที่มีการแข่งขัน ล็อกที่ประสิทธิภาพแย่อาจสร้างผลกระทบเชิงระบบที่เลวร้ายมาก เช่นทำให้เกิด hotspot ใน memory network และสิ่งนี้ก็น่าจะปรากฏให้เห็นที่นี่ด้วย
    • ผมไม่ค่อยเห็นด้วยเต็มที่กับการประเมินว่า “ไม่ควรให้หลายเธรดคอยทุบ mutex ตัวเดียวกัน”
      มีหลายกรณีที่นึกออกซึ่งหลายเธรดมารุม mutex ตัวเดียวกัน ตัวอย่างง่าย ๆ คือการเติมข้อมูลลงโครงสร้างข้อมูลอย่าง list หรือ dictionary พร้อมกัน
      ใช้ message passing ก็ทำได้ แต่ก็อาจใช้หน่วยความจำมากกว่า และช้ากว่าการรอเพื่อเขียนลงตำแหน่งร่วมกัน
  • production ไม่ได้เกี่ยวกับความเร็ว ประสิทธิภาพ หรือสิ่งที่เห็นชัดว่าเป็น “แฮ็กฉลาด ๆ”
    ถ้าต้องสละประสิทธิภาพ 50% เพื่อรับประกันว่าจะไม่ถูกเรียกให้ไปซ่อมระบบที่พังตอนตี 3 วันอาทิตย์ ผมก็จะเลือกแบบนั้นทุกครั้ง
    production เป็นเรื่องของ ความน่าเชื่อถือ และการเขียนโค้ดที่เชื่อถือได้ยากกว่าการเขียนโค้ดที่ “เร็ว” ถึง 10 เท่า