การสืบสวนล่าสุดเกี่ยวกับปัญหาประสิทธิภาพของ 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)