ภาษาโปรแกรมมิ่ง Rust ได้จุดประกายการถกเถียงอย่างรุนแรงเกี่ยวกับความเร็วในการคอมไพล์ โดยนักพัฒนาได้แบ่งปันประสบการณ์ที่หลากหลายตั้งแต่ความหงุดหงิดไปจนถึงการยอมรับ การวิเคราะห์ล่าสุดเกี่ยวกับการ build Rust ที่ช้าเผยให้เห็นว่าปัญหามักเกิดจากปัญหาการกำหนดค่า Docker มากกว่าข้อจำกัดพื้นฐานของภาษา แม้ว่าการอภิปรายในวงกว้างจะได้เน้นย้ำถึงข้อแลกเปลี่ยนที่สำคัญในการออกแบบ compiler สมัยใหม่
ปัญหาการกำหนดค่า Docker ที่อยู่เบื้องหลังการ Build ที่ช้า
ตัวการหลักในหลายกรณีของการคอมไพล์ Rust ที่ช้าดูเหมือนจะเป็นการ incremental build ที่เสียหายภายใน Docker container เมื่อนักพัฒนา build โปรเจ็กต์ Rust ภายใน Docker โดยไม่มีการ mount volume หรือการกำหนดค่า cache ที่เหมาะสม compiler จะสูญเสียความสามารถในการนำ dependency ที่คอมไพล์แล้วมาใช้ซ้ำ สิ่งนี้บังคับให้เกิดการ rebuild ทั้งหมดของ dependency tree ทั้งหมด เปลี่ยนสิ่งที่ควรจะเป็นการอัปเดต incremental ที่รวดเร็วให้กลายเป็นการคอมไพล์แบบเต็มที่ใช้เวลานาน วิธีแก้ไขมักเกี่ยวข้องกับการใช้ bind mount การ cache layer ของ Docker ที่เหมาะสม หรือการ build นอก container และคัดลอกเฉพาะไฟล์ binary สุดท้าย
ระบบ filesystem layer ของ Docker สามารถสร้างโอเวอร์เฮดเพิ่มเติมเมื่อต้องรับมือกับโมเดลการคอมไพล์ของ Rust ซึ่งสร้างไฟล์กลางจำนวนมากระหว่างกระบวนการ build ปฏิสัมพันธ์ระหว่าง filesystem แบบ copy-on-write ของ Docker และ incremental compilation cache ของ Rust สามารถส่งผลให้เกิดการลดประสิทธิภาพที่ทำให้การ build ดูช้ากว่าที่เป็นจริง
กลยุทธ์การปรับปรุง Docker Build ให้เหมาะสม:
- ใช้ bind mounts สำหรับ source code และ target directories
- ใช้งาน Docker layer caching สำหรับ dependencies อย่างเหมาะสม
- Build นอก container และ copy เฉพาะ binary เท่านั้น
- ใช้ multi-stage Dockerfiles เพื่อแยก build และ runtime environments
- กำหนดค่า cargo ด้วย
--target-dir
สำหรับ persistent build caches
ตัวเลือกการออกแบบภาษาส่งผลต่อความเร็วการคอมไพล์
นอกเหนือจากปัญหา Docker แล้ว ความเร็วการคอมไพล์ของ Rust สะท้อนถึงการตัดสินใจออกแบบที่ตั้งใจให้ความสำคัญกับประสิทธิภาพ runtime และความปลอดภัยมากกว่าความเร็วในการ build ภาษานี้ใช้ monomorphization ซึ่งเป็นกระบวนการที่ฟังก์ชัน generic ถูกคอมไพล์เป็น machine code แยกต่างหากสำหรับแต่ละประเภทที่ใช้งาน สิ่งนี้สร้าง runtime code ที่ปรับให้เหมาะสมสูง แต่ต้องการงานคอมไพล์อย่างมาก นอกจากนี้ borrow checker และระบบ trait ที่ซับซ้อนของ Rust ยังทำการวิเคราะห์ที่ซับซ้อนเพื่อให้แน่ใจว่ามีความปลอดภัยของหน่วยความจำและป้องกัน data race
การอภิปรายในชุมชนเผยให้เห็นว่าเวลาการคอมไพล์ส่วนใหญ่จริงๆ แล้วใช้ไปกับ LLVM ซึ่งเป็น backend code generator มากกว่าฟีเจอร์เฉพาะของ Rust เช่น borrow checker Compiler สร้าง intermediate code จำนวนมากที่ LLVM ต้องทำการปรับให้เหมาะสมออกไป ทำให้เกิดคอขวดใน compilation pipeline
ปัญหาคอขวดหลักในการคอมไพล์ Rust:
- LLVM backend: มากกว่า 80% ของเวลาการคอมไพล์ใน release builds
- Monomorphization: สร้างสำเนาหลายชุดของโค้ด generic สำหรับ type ที่แตกต่างกัน
- การคอมไพล์ dependency: dependency tree ขนาดใหญ่ต้องใช้เวลาการคอมไพล์เริ่มต้นมาก
- ค่าใช้จ่าย filesystem ของ Docker: layer แบบ copy-on-write รบกวนการ build แบบ incremental
- Borrow checker: น้อยกว่า 1% ของเวลาการคอมไพล์ทั้งหมด (ตรงข้ามกับความเชื่อทั่วไป)
แนวทางทางเลือกและโซลูชันที่เกิดขึ้นใหม่
นักออกแบบภาษาบางคนกำลังใช้แนวทางที่แตกต่างเพื่อสร้างสมดุลระหว่างความเร็วการคอมไพล์กับฟีเจอร์อื่นๆ ตัวอย่างเช่น Zig ได้บรรลุเวลาการคอมไพล์ที่รวดเร็วอย่างน่าทึ่งโดยการพัฒนา custom backend แทนการพึ่งพา LLVM สำหรับ debug build แนวทางนี้ช่วยให้การทำซ้ำระหว่างการพัฒนาเร็วขึ้นมาก แม้ว่าจะต้องการการลงทุนด้านวิศวกรรมอย่างมากในการรักษา backend การสร้างโค้ดหลายตัว
ระบบนิเวศ Rust กำลังตอบสนองด้วยเครื่องมือเช่น Cranelift ซึ่งเป็น code generator ทางเลือกที่สามารถลดเวลาการคอมไพล์สำหรับ development build ได้อย่างมีนัยสำคัญ โซลูชัน hot-reloading ก็กำลังเกิดขึ้นเช่นกัน ช่วยให้นักพัฒนาสามารถแก้ไขโปรแกรมที่กำลังทำงานโดยไม่ต้องผ่านรอบการคอมไพล์แบบเต็ม
การเปรียบเทียบเวลาในการ Compile:
- C unity build (278k บรรทัด): ~1.5 วินาทีสำหรับการ compile แบบ clean
- Rust incremental build: ~0.54 วินาที (เมื่อทำงานได้อย่างถูกต้อง)
- Rust Docker build (incremental เสีย): ช้ากว่า local builds 130 เท่า
- Rust กับ Cranelift backend: เร็วกว่า LLVM 4 เท่า (16s → 4s สำหรับการพัฒนาเกม)
มุมมองของชุมชนเกี่ยวกับข้อแลกเปลี่ยนที่ยอมรับได้
ความคิดเห็นของนักพัฒนามีความหลากหลายอย่างกว้างขวางเกี่ยวกับว่าความเร็วการคอมไพล์ของ Rust นั้นยอมรับได้หรือไม่ นักพัฒนาหลายคนที่มาจากพื้นฐาน C++ พบว่าเวลา build ของ Rust นั้นสมเหตุสมผล เนื่องจากเคยประสบกับรอบการคอมไพล์ที่นานกว่ามากกับโค้ด C++ ที่ใช้ template อย่างหนัก คนอื่นๆ โดยเฉพาะผู้ที่ทำงานกับแอปพลิเคชันแบบ interactive เช่นเกม พบว่าแม้แต่เวลา build ปานกลางก็รบกวนขั้นตอนการพัฒนาของพวกเขา
ยิ่ง compiler ทำอะไรให้คุณมากในเวลา build ก็ยิ่งใช้เวลา build นานขึ้น มันง่ายๆ แค่นั้น
ฉันทามติในหมู่นักพัฒนา Rust ที่มีประสบการณ์ชี้ให้เห็นว่าปัญหาความเร็วการคอมไพล์มักจะจัดการได้ผ่านโครงสร้างโปรเจ็กต์ที่เหมาะสม การจัดการ dependency และการกำหนดค่า build อย่างไรก็ตาม การออกแบบของภาษาโดยธรรมชาติให้ความสำคัญกับประสิทธิภาพ runtime และการรับประกันความปลอดภัยมากกว่าความเร็วการคอมไพล์ ทำให้เหมาะสมน้อยกว่าสำหรับเวิร์กโฟลว์ที่ต้องการรอบการทำซ้ำที่เร็วมาก
มองไปข้างหน้า
ในขณะที่ Rust ยังคงเติบโต ชุมชนให้ความสำคัญกับการปรับปรุงประสิทธิภาพการคอมไพล์มากขึ้น ความพยายามต่างๆ รวมถึงการ incremental compilation ที่ดีขึ้น การปรับปรุงการคอมไพล์แบบขนาน และ backend การสร้างโค้ดทางเลือกสำหรับ development build อย่างไรก็ตาม ฟีเจอร์พื้นฐานของภาษาที่ส่งผลให้การคอมไพล์ช้าไม่น่าจะเปลี่ยนแปลง เนื่องจากให้การรับประกันความปลอดภัยและประสิทธิภาพที่ทำให้ Rust น่าสนใจสำหรับการเขียนโปรแกรมระบบ
การอภิปรายที่กำลังดำเนินอยู่เน้นย้ำถึงแนวโน้มที่กว้างขึ้นในการออกแบบภาษาโปรแกรมมิ่ง ซึ่งนักพัฒนาต้องเลือกระหว่างข้อแลกเปลี่ยนที่แตกต่างกัน: ความเร็วการคอมไพล์ ประสิทธิภาพ runtime การรับประกันความปลอดภัย และผลิตภาพของนักพัฒนา ตำแหน่งของ Rust ในสเปกตรัมนี้ยังคงพัฒนาไปเรื่อยๆ ขณะที่เครื่องมือปรับปรุงและแนวปฏิบัติที่ดีที่สุดเกิดขึ้น
อ้างอิง: Why is the Rust compiler so slow?