ตัวจัดสรรหน่วยความจำของ Musl ทำให้ประสิทธิภาพลดลง 700 เท่าในแอปพลิเคชันแบบหลายเธรด

ทีมชุมชน BigGo
ตัวจัดสรรหน่วยความจำของ Musl ทำให้ประสิทธิภาพลดลง 700 เท่าในแอปพลิเคชันแบบหลายเธรด

การสืบสวนล่าสุดเกี่ยวกับปัญหาประสิทธิภาพของ musl libc ได้เผยผลลัพธ์ที่น่าตกใจซึ่งอาจส่งผลกระทบต่อแอปพลิเคชันนับไม่ถ้วน ไลบรารี C ชื่อ musl ที่ใช้กันทั่วไปใน Alpine Linux containers และ static builds มีตัวจัดสรรหน่วยความจำที่อาจทำให้โปรแกรมแบบหลายเธรดทำงานช้าลงอย่างมาก ในกรณีที่รุนแรงที่สุด แอปพลิเคชันมีประสิทธิภาพลดลงถึง 700 เท่าเมื่อเทียบกับที่คาดหวัง

ผลกระทบต่อประสิทธิภาพอย่างรุนแรงตามจำนวน Core

เครื่อง 6-core: ช้าลง 7 เท่า เครื่อง 8-core: ช้าลง 4 เท่า
เครื่อง 48-core: ช้าลง 700 เท่า

การลดลงของประสิทธิภาพจะเพิ่มขึ้นอย่างมากเมื่อมี CPU core มากขึ้น เนื่องจาก thread จำนวนมากต้องแข่งขันกันเพื่อใช้ allocator lock เดียวกัน อินสแตนซ์ cloud สมัยใหม่สามารถมี core มากกว่า 192 ตัว ทำให้ปัญหานี้รุนแรงยิ่งขึ้นสำหรับแอปพลิเคชันที่ต้องการความสามารถในการขยายตัว

สาเหตุหลัก: การแย่งชิงล็อก

ปัญหาประสิทธิภาพเกิดจากการออกแบบตัวจัดสรรของ musl ที่ใช้ล็อกที่ใช้ร่วมกันเพียงตัวเดียวสำหรับการดำเนินการหน่วยความจำทั้งหมด เมื่อหลายเธรดพยายามจัดสรรหรือปลดปล่อยหน่วยความจำในเวลาเดียวกัน พวกมันต้องรอกันและกัน ทำให้เกิดคอขวด การออกแบบนี้ทำงานได้ดีสำหรับโปรแกรมเธรดเดียว แต่กลายเป็นปัญหาใหญ่เมื่อมี CPU cores และเธรดมากขึ้นเข้ามาเกี่ยวข้อง

ชุมชนได้ระบุว่าตัวจัดสรรของ musl อาศัย heap เดียวที่มีการล็อกเพื่อรองรับหลายเธรด หมายความว่าการดำเนินการหน่วยความจำแต่ละครั้งต้องได้รับล็อกนี้ ทางเลือกสมัยใหม่อย่าง mimalloc ใช้ heap แยกตามเธรดแทน โดยแต่ละเธรดจัดการพื้นที่หน่วยความจำของตัวเอง วิธีการนี้ทำงานได้ดีเป็นพิเศษกับภาษาโปรแกรมอย่าง Rust ที่ออบเจ็กต์ไม่ค่อยย้ายระหว่างเธรด

ผลกระทบในโลกจริงข้ามโปรเจ็กต์ต่างๆ

ปัญหานี้ไม่ใหม่ แต่ยังคงทำให้นักพัฒนาตกใจ หลายโปรเจ็กต์ได้บันทึกปัญหาที่คล้ายกัน โดยมีความช้าลงตั้งแต่ 2 เท่าถึง 20 เท่าในแอปพลิเคชันทั่วไป ความแตกต่างขึ้นอยู่กับจำนวนเธรดที่แข่งขันกันเพื่อใช้หน่วยความจำและความถี่ในการจัดสรรหน่วยความจำใหม่

หลายโปรเจ็กต์หลักได้เปลี่ยนจากตัวจัดสรรเริ่มต้นของ musl หรือละทิ้ง musl ไปโดยสิ้นเชิง ผลกระทบต่อประสิทธิภาพจะรุนแรงมากขึ้นในระบบหลาย core สมัยใหม่ ที่แอปพลิเคชันใช้เธรดมากขึ้นตามธรรมชาติเพื่อใช้ประโยชน์จากพลังการประมวลผลที่มีอยู่

การเปรียบเทียบประสิทธิภาพ: glibc vs musl

เมตริก glibc musl ความแตกต่าง
เวลาผู้ใช้ (วินาที) 1.31 2.72 ช้ากว่า 2.1 เท่า
เวลาระบบ (วินาที) 0.32 6.13 ช้ากว่า 19.2 เท่า
เวลาที่ใช้ทั้งหมด (วินาที) 0.17 1.18 ช้ากว่า 7 เท่า
การสลับบริบทแบบสมัครใจ 1,196 159,786 มากกว่า 167 เท่า
การใช้งาน CPU 943% 745% มีประสิทธิภาพน้อยกว่า 21%

ตัวจัดสรรใหม่ไม่ช่วยอะไร

นักพัฒนาหลายคนหวังว่าตัวจัดสรร mallocng ใหม่ของ musl ที่เปิดตัวในเวอร์ชัน 1.2.1 จะแก้ปัญหาประสิทธิภาพเหล่านี้ น่าเสียดายที่การทดสอบแสดงให้เห็นว่ามันไม่ได้สร้างความแตกต่างมากนักสำหรับแอปพลิเคชันแบบหลายเธรด ทีมพัฒนา musl ออกแบบตัวจัดสรรใหม่เพื่อให้ความสำคัญกับการใช้หน่วยความจำต่ำและความปลอดภัยมากกว่าประสิทธิภาพดิบ

ตัวจัดสรร mallocng ได้รับการออกแบบเพื่อให้ความสำคัญกับการใช้หน่วยความจำที่ต่ำมาก ต้นทุนการกระจัดกระจายในกรณีเลวร้ายที่สุดที่ต่ำ และการเสริมความแข็งแกร่งที่แข็งแกร่งมากกว่าประสิทธิภาพ

ตัวเลือกการออกแบบนี้สะท้อนปรัชญาของ musl ในการให้ค่าเริ่มต้นที่ปลอดภัยในขณะที่อนุญาตให้แอปพลิเคชันเลือกใช้ตัวจัดสรรที่เร็วกว่าเมื่อจำเป็น

มีวิธีแก้ไขง่ายๆ

โชคดีที่การแก้ไขปัญหานี้เป็นเรื่องง่ายสำหรับแอปพลิเคชันส่วนใหญ่ นักพัฒนาสามารถเปลี่ยนไปใช้ตัวจัดสรรทางเลือกอย่าง mimalloc หรือ jemalloc ได้อย่างง่ายดาย ซึ่งจัดการกับงานแบบหลายเธรดได้ดีกว่ามาก สำหรับโปรเจ็กต์ Rust การเพิ่มไม่กี่บรรทัดลงในไฟล์การกำหนดค่าสามารถแก้ปัญหาได้โดยสิ้นเชิง

การแก้ไขเกี่ยวข้องกับการใช้ตัวจัดสรรที่แตกต่างกันแบบมีเงื่อนไขเฉพาะเมื่อสร้างด้วย musl เท่านั้น ดังนั้นจึงไม่ส่งผลกระทบต่อแพลตฟอร์มอื่น วิธีการนี้ให้ประโยชน์จากขนาดเล็กและความเข้ากันได้ข้ามแพลตฟอร์มของ musl โดยไม่มีการลดประสิทธิภาพ

วิธีแก้ไขด่วนสำหรับโปรเจกต์ Rust

เพิ่มบรรทัดเหล่านี้ลงในไฟล์ Cargo.toml ของคุณเพื่อหลีกเลี่ยงปัญหาประสิทธิภาพของ musl allocator:

 หลีกเลี่ยง default allocator ของ musl เนื่องจากปัญหา lock contention
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "1.4.0"

ตัวเลือก allocator อื่น ๆ:

  • mimalloc: การออกแบบ heap แบบ per-thread ที่ทันสมัย
  • jemalloc: ทางเลือกที่เป็นผู้ใหญ่และใช้กันอย่างแพร่หลาย
  • tcmalloc: thread-caching malloc ของ Google

เมื่อประสิทธิภาพมีความสำคัญ

นักพัฒนาที่มีประสบการณ์บางคนโต้แย้งว่าการเจอขีดจำกัดประสิทธิภาพของตัวจัดสรรบ่งบอกถึงการออกแบบโปรแกรมที่ไม่ดี พวกเขาแนะนำว่าโค้ดที่เขียนได้ดีควรลดการจัดสรรหน่วยความจำในส่วนที่สำคัญต่อประสิทธิภาพให้น้อยที่สุด แม้ว่าคำแนะนำนี้จะมีข้อดี แต่ก็ไม่ได้คำนึงถึงความเป็นจริงที่ว่าแอปพลิเคชันหลายตัวต้องทำงานได้ดีกับแนวทางการเขียนโค้ดที่สมเหตุสมผล ไม่ใช่เฉพาะที่ได้รับการปรับให้เหมาะสมอย่างสมบูrณ์เท่านั้น

การสนทนาในชุมชนเผยให้เห็นความแตกแยกระหว่างผู้ที่มองว่านี่เป็นข้อบกพร่องพื้นฐานและผู้ที่มองว่าเป็นการแลกเปลี่ยนที่ยอมรับได้สำหรับประโยชน์อื่นๆ ของ musl สำหรับแอปพลิเคชันที่ไม่ได้จัดสรรหน่วยความจำบ่อยหรือใช้เธรดน้อยกว่า ผลกระทบต่อประสิทธิภาพอาจไม่มีนัยสำคัญ

อ้างอิง: Default musl allocator considered harmful (to performance)