การปรับปรุงประสิทธิภาพ Rust FizzBuzz เผยให้เห็น I/O เป็นคอขวดด้านประสิทธิภาพ 99.8%

ทีมชุมชน BigGo
การปรับปรุงประสิทธิภาพ Rust FizzBuzz เผยให้เห็น I/O เป็นคอขวดด้านประสิทธิภาพ 99.8%

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

กลยุทธ์การจัดสรรหน่วยความจำแสดงผลลัพธ์ที่หลากหลาย

การเดินทางสู่การปรับปรุงประสิทธิภาพเริ่มต้นด้วยความพยายามในการขจัด heap allocations โดยใช้ stack-based buffers แทนการใช้ dynamic strings อย่างไรก็ตาม วิธี heapless ที่ใช้ byte arrays ขนาดคงที่กลับมีประสิทธิภาพแย่กว่าการใช้งานเดิม โดยใช้เวลา 39.4 microseconds เมื่อเทียบกับประสิทธิภาพของเวอร์ชัน heap-based ผลลัพธ์ที่ขัดกับสัญชาตญาณนี้เน้นย้ำว่า memory allocators สมัยใหม่สามารถมีประสิทธิภาพเหนือกว่าการจัดการ buffer ด้วยตนเองสำหรับการจัดสรรที่เล็กและบ่อย

สมาชิกชุมชนได้ชี้ให้เห็นโอกาสในการปรับปรุงประสิทธิภาพเพิ่มเติมที่ไม่ได้สำรวจในการวิเคราะห์เดิม ผู้แสดงความคิดเห็นคนหนึ่งสังเกตว่าการดำเนินการ modulo แม้จะปรากฏเป็นคำสั่ง CPU เดียว แต่จริงๆ แล้วใช้เวลาหลายสิบ cycles บนโปรเซสเซอร์ x86 และมักถูกปรับปรุงโดย compilers ให้เป็นการดำเนินการ multiply-and-shift เมื่อจัดการกับค่าคงที่

ผลการเปรียบเทียบประสิทธิภาพ:

  • การใช้งาน Python แบบเดิม: ประมาณ 106-110 ไมโครวินาที
  • เวอร์ชัน Rust พื้นฐาน: 59.5 ± 0.6 ไมโครวินาที
  • การทดลอง Heapless Rust: 39.4 ± 0.5 ไมโครวินาที (ช้ากว่า)
  • การส่งออกแบบ Buffered: ประมาณ 42 ไมโครวินาที (ปรับปรุงได้ 17 ไมโครวินาที)
  • การปรับปรุงแบบไม่มี newlines: 9.6 ± 0.3 ไมโครวินาที (ปรับปรุงได้ 80%)
  • การคำนวณเท่านั้น (ไม่มี I/O): 388 นาโนวินาที

การแสดงผลบน Terminal ครอบงำเวลาการประมวลผล

การค้นพบที่โดดเด่นที่สุดเกิดขึ้นเมื่อวัดประสิทธิภาพโดยไม่มี print statements การลบ output ทั้งหมดลดเวลาการประมวลผลเหลือเพียง 388 nanoseconds เผยให้เห็นว่า 99.8% ของ runtime ถูกใช้ไปกับการดำเนินการ terminal I/O การค้นพบนี้เปลี่ยนแปลงวิธีคิดของเราเกี่ยวกับการปรับปรุงประสิทธิภาพของโปรแกรมดังกล่าวอย่างพื้นฐาน

การกระทำง่ายๆ ของการลบ readlines และพิมพ์ buffer ทั้งหมดเป็นบล็อกต่อเนื่องขนาด 8kb แทนการจัดสรรบรรทัดใหม่ให้กับแต่ละหมายเลข ลด runtime ของเราลงประมาณ 80%

กลยุทธ์ buffering พิสูจน์ให้เห็นว่ามีประสิทธิภาพมากกว่าการปรับปรุงอัลกอริทึม การเขียน output ลงใน buffer เดียวและ flush ครั้งเดียวลด runtime ลงเกือบหนึ่งในสี่ การอภิปรายในชุมชนเผยให้เห็นเทคนิคการปรับปรุงประสิทธิภาพ I/O เพิ่มเติม รวมถึงการใช้ stdout locks และ BufWriter wrappers เพื่อสร้างสมดุลระหว่างความเร็วกับการใช้หน่วยความจำ

แนวทางทางเลือกเกิดขึ้นจากชุมชน

การอภิปรายได้สร้างกลยุทธ์การปรับปรุงประสิทธิภาพทางเลือกหลายแบบ นักพัฒนาบางคนแนะนำให้ใช้ circular data structures หรือ wheels เพื่อขจัดการดำเนินการ modulo ทั้งหมด โดยมีการใช้งานของนักเรียนมัธยมปลายรายงานว่าเพิ่มประสิทธิภาพเป็นสองเท่าโดยใช้วิธีการนี้ คนอื่นๆ เสนอเทคนิค loop unrolling ที่สามารถข้ามการตรวจสอบ modulo โดยประมวลผลบล็อกของ 15 iterations ในครั้งเดียว

สำหรับเวอร์ชันขยายที่รองรับตัวหารเพิ่มเติมเช่น 7 สำหรับ Baz สมาชิกชุมชนอภิปรายเกี่ยวกับการแลกเปลี่ยนระหว่างความสามารถในการบำรุงรักษาและประสิทธิภาพ แม้ว่า loop unrolling จะทำงานได้ดีสำหรับเวอร์ชันคลาสสิก 3-และ-5 แต่มันจะซับซ้อนมากขึ้นเมื่อรองรับตัวหารที่กำหนดเอง

การปรับขนาดการประมวลผลแบบขนาน:

  • แบบอนุกรม (N=100): 2.43 ± 0.05 ไมโครวินาที
  • แบบขนาน (N=100): 44.23 ± 4.96 ไมโครวินาที (ช้ากว่า 18 เท่าเนื่องจาก overhead)
  • แบบอนุกรม (N=100,000): 2.93 ± 0.08 มิลลิวินาที
  • แบบขนาน (N=100,000): 1.45 ± 0.15 มิลลิวินาที (เร็วขึ้น 2 เท่า)

การประมวลผลแบบขนานแสดงผลตอบแทนที่ลดลง

การทดลองการประมวลผลแบบขนานเผยให้เห็นว่า multi-threading มีประโยชน์เฉพาะในระดับที่ใหญ่กว่ามาก สำหรับ FizzBuzz มาตรฐาน 100 iterations การประมวลผลแบบขนานจริงๆ แล้วช้ากว่าเนื่องจาก thread startup overhead การเพิ่มประสิทธิภาพปรากฏเฉพาะเมื่อประมวลผล 100,000 iterations โดยบรรลุความเร็วเพิ่มขึ้นประมาณ 2 เท่าบนระบบ multi-core

การสำรวจสรุปด้วยการใช้งาน procedural macro ที่สร้างโค้ดที่ปรับปรุงแล้วในเวลา compile แม้ว่าวิธีการนี้จะแสดงความสามารถ metaprogramming ของ Rust แต่ feedback จากชุมชนแนะนำว่าการสร้าง static string literals อาจจะปฏิบัติได้มากกว่าการสร้างโค้ดในเวลา runtime

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

อ้างอิง: Optimizing FizzBuzz in Rust for Fun and Profit