1 คะแนน โดย GN⁺ 2025-06-28 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เมื่อต้อง build เว็บไซต์ที่เขียนด้วย Rust ซ้ำ ๆ ด้วย Docker ก็พบปัญหาเรื่องเวลาในการ build
  • ในการตั้งค่า Docker แบบพื้นฐาน จะเกิดการ build dependency ทั้งหมดใหม่ ทุกครั้ง ทำให้ใช้เวลามากกว่า 4 นาที
  • แม้จะใช้ cargo-chef และเครื่องมือแคชแล้ว ก็ยังต้องใช้เวลา มากอยู่ดีในการ build ไบนารีสุดท้าย
  • จากการทำ profiling พบว่าใช้เวลาส่วนใหญ่ไปกับ LTO (link-time optimization) และ LLVM module optimization
  • แม้จะปรับตัวเลือก optimization, ข้อมูลดีบัก และการตั้งค่า LTO เพื่อช่วยให้ดีขึ้นได้บางส่วน แต่ก็ยังพบว่าการคอมไพล์ไบนารีสุดท้ายต้องใช้เวลาอย่างน้อย 50 วินาที

การตั้งคำถามและเบื้องหลัง

  • ทุกครั้งที่แก้ไขเว็บไซต์ส่วนตัวที่เขียนด้วย Rust ต้องทำงานซ้ำ ๆ ที่ยุ่งยากคือ build ไบนารีแบบ static link แล้วคัดลอกไปยังเซิร์ฟเวอร์ก่อนรีสตาร์ต
  • แม้ตั้งใจจะเปลี่ยนไปใช้การ deploy แบบคอนเทนเนอร์ เช่น Docker หรือ Kubernetes แต่ ความเร็วในการ build Rust บน Docker กลับกลายเป็นปัญหาใหญ่
  • ใน Docker ต่อให้แก้โค้ดเพียงเล็กน้อย ก็ต้อง build ใหม่ทั้งหมดตั้งแต่ต้น ทำให้เกิดความไม่มีประสิทธิภาพ

การ build Rust บน Docker – แนวทางพื้นฐาน

  • แนวทาง Dockerfile ทั่วไปคือคัดลอก dependency และซอร์สโค้ดทั้งหมด แล้วจึงรัน cargo build
  • ในกรณีนี้จะไม่ได้ประโยชน์จากแคช จึงเกิดการ build ใหม่ทั้งหมด ซ้ำไปซ้ำมา
  • สำหรับเว็บไซต์ของผู้เขียนเอง การ build ทั้งหมดใช้เวลา ประมาณ 4 นาที—และยังต้องใช้เวลาเพิ่มในการดาวน์โหลด dependency

ปรับปรุงแคชการ build บน Docker – cargo-chef

  • เครื่องมือ cargo-chef ช่วยให้สามารถ แคช เฉพาะ dependency แยกไว้ล่วงหน้าในคนละเลเยอร์ได้
  • ด้วยวิธีนี้ เมื่อโค้ดเปลี่ยนก็สามารถนำการ build dependency เดิมกลับมาใช้ซ้ำได้ จึงคาดหวังผลเรื่อง การปรับปรุงความเร็วในการ build
  • เมื่อนำมาใช้จริง พบว่าในเวลารวมทั้งหมด มีเพียง 25% เท่านั้นที่อยู่กับการ build dependency และยังคงใช้เวลามากกับการ build ไบนารีเว็บเซอร์วิสตัวสุดท้าย อยู่ดี (2 นาที 50 วินาทีถึง 3 นาที)
  • แม้จะประกอบด้วย dependency หลักอย่าง axum, reqwest, tokio-postgres ฯลฯ และโค้ดที่เขียนเองราว 7,000 บรรทัด แต่การรัน rustc เพียงครั้งเดียวก็ยังใช้เวลาถึง 3 นาที

วิเคราะห์เวลา build ของ rustc: cargo --timings

  • สามารถใช้ cargo --timings เพื่อตรวจสอบเวลา build ของแต่ละ crate (compilation unit) ได้
  • ผลลัพธ์ยืนยันว่า การ build ไบนารีสุดท้าย กินเวลาส่วนใหญ่ของทั้งหมด
  • แม้จะช่วยวิเคราะห์สาเหตุได้ละเอียดขึ้น แต่ก็ยังไม่เพียงพอสำหรับการมองเห็นการทำงานภายในของคอมไพเลอร์อย่างเจาะจง

ใช้การ profiling ภายในของ rustc เอง (-Zself-profile)

  • เปิดใช้ความสามารถ profiling ภายในของ rustc ด้วยแฟล็ก -Zself-profile เพื่อวัดเวลาการทำงานแบบละเอียด
  • โดยเปิดใช้งาน profiling ผ่านตัวแปรสภาพแวดล้อม
  • เมื่อนำผลไปวิเคราะห์ด้วยเครื่องมือสรุปผล (summarize) ก็พบว่า LLVM LTO (link-time optimization) และ LLVM module code generation กินเวลามากกว่า 60% ของทั้งหมด
  • การแสดงผลแบบ flamegraph ก็ยืนยันเช่นกันว่าในขั้น codegen_module_perform_lto มีการใช้เวลารวมถึง 80%

LTO (link-time optimization) และตัวเลือก optimization ของการ build

  • การ build ของ Rust โดยปกติจะถูกแบ่งตาม codegen unit ก่อน จากนั้น LTO จึงค่อยนำ optimization ระดับทั้งโปรแกรมมาใช้ในช่วงค่อนข้างท้าย
  • LTO มีหลายตัวเลือก เช่น off, thin, fat ซึ่งแต่ละแบบส่งผลต่อทั้งประสิทธิภาพและผลลัพธ์สุดท้าย
  • โปรเจ็กต์ของผู้เขียนตั้งค่า LTO เป็น thin ใน Cargo.toml และตั้ง debug symbol เป็น full
  • เมื่อลองทดสอบชุดค่าผสมต่าง ๆ ของ LTO/debug symbol พบว่า:
    • debug symbol แบบ full ทำให้เวลา build เพิ่มขึ้น และ fat LTO ทำให้ build ช้าลงราว 4 เท่า
    • ต่อให้เอา LTO และ debug symbol ออก ก็ยังต้องใช้เวลา อย่างน้อย 50 วินาที ในการ build

การปรับแต่งเพิ่มเติมและข้อสังเกต

  • เวลาราว 50 วินาทีอาจไม่ใช่ปัญหาใหญ่นักสำหรับเว็บไซต์ของผู้เขียนที่แทบไม่มีภาระใช้งานจริง แต่ด้วยความอยากรู้อยากเห็นทางเทคนิคจึงพยายามวิเคราะห์ต่อ
  • หากนำ incremental compilation มาใช้กับ Docker ได้อย่างเหมาะสม ก็อาจทำให้ build ได้เร็วขึ้นอีก แต่ต้องผสานทั้งความสะอาดของสภาพแวดล้อมการ build และแคชของ Docker ให้ดี

การ profiling เชิงลึกของขั้นตอน LLVM

  • ต่อให้ปิด LTO และ debug symbol แล้ว ขั้น LLVM_module_optimize ก็ยังใช้เวลาเกือบ 70%
  • จึงตระหนักว่าค่าเริ่มต้นของ opt-level (3) ใน release profile มีต้นทุนด้าน optimization สูง และทดลองลด opt-level เฉพาะกับไบนารี
  • จากการทดลองชุดค่าผสมของ optimization แบบต่าง ๆ พบว่า เมื่อไม่ใช้ optimization เลย (opt-level=0) จะใช้เวลาราว 15 วินาที แต่เมื่อเปิด optimization (1~3) จะใช้เวลาราว 50 วินาที

วิเคราะห์เชิงลึกของ LLVM trace event

  • สามารถใช้แฟล็กเพิ่มเติมของ rustc (-Z time-llvm-passes, -Z llvm-time-trace) เพื่อติดตามเวลาในแต่ละขั้นของ LLVM ได้อย่างละเอียด
  • -Z time-llvm-passes ให้ผลลัพธ์จำนวนมากจนเกินข้อจำกัดของ log ใน Docker ได้บ่อย จึงอาจต้องปรับการตั้งค่า log
  • หากบันทึก log ลงไฟล์เพื่อนำมาวิเคราะห์ ก็จะสามารถตรวจสอบเวลาแยกตาม LLVM optimization pass ได้ทีละรายการ
  • ตัวเลือก -Z llvm-time-trace จะสร้างเอาต์พุต JSON ขนาดใหญ่มากในรูปแบบ chrome tracing ทำให้เปิดด้วยเครื่องมือแก้ไข/วิเคราะห์ข้อความทั่วไปได้ยาก
  • แต่สามารถแยกประมวลผลเป็นหน่วยบรรทัด (jsonl) เพื่อวิเคราะห์ในสภาพแวดล้อมแบบ CLI/สคริปต์ได้

ประเด็นสำคัญและบทสรุป

  • เมื่อ build โปรเจ็กต์ Rust ที่ซับซ้อนด้วย Docker คอขวดของความเร็วในการ build มักอยู่ที่การ build ไบนารีสุดท้าย และขั้น optimization ของ LLVM ที่เกี่ยวข้อง
  • เมื่อปรับ LTO, debug symbol และ opt-level จะเห็น trade-off ระหว่างเวลา build กับขนาดไบนารีอย่างชัดเจน
  • หากปรับตัวเลือก optimization อย่างจริงจัง ก็สามารถ ลดเวลา build ได้มาก แต่หากไม่ใช้ optimization เลย ก็อาจทำให้ประสิทธิภาพลดลงได้
  • หากต้องให้ความสำคัญกับประสิทธิภาพการ build ในสภาพแวดล้อม production หรือในโปรเจ็กต์ที่มี dependency ระดับใหญ่ ควรใช้ profiling อย่างจริงจังเพื่อ ระบุคอขวดเชิงลึกให้ชัดเจน
  • ในการออกแบบ build pipeline ของ Rust จำเป็นต้องวาง ชุดค่าผสมของ LTO, opt-level, debug symbol และกลยุทธ์แคช อย่างละเอียดรอบคอบ

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

 
GN⁺ 2025-06-28
ความเห็นบน Hacker News
  • โปรเจ็กต์ Rust มักดูเล็กกว่าความเป็นจริงจึงน่าสนใจอยู่บ่อย ๆ อย่างแรกคือ dependency ไม่ได้เชื่อมโยงตรงกับขนาดจริงของ codebase เสมอไป ใน C++ โปรเจ็กต์ใหญ่ ๆ มัก vendor dependency เข้ามาเองหรือไม่ใช้เลยก็มี ดังนั้นถ้ามีโค้ด 400,000 บรรทัดแล้วมันช้า ก็อาจคิดได้ว่า "โค้ดเยอะก็ช้าเป็นธรรมดา" อย่างที่สอง ส่วนที่เป็นปัญหากว่ามากคือ macro ถ้า macro ขยายซ้ำทีละ 10 หรือ 100 บรรทัด โปรเจ็กต์ 10,000 บรรทัดก็อาจกลายเป็นล้านบรรทัดได้อย่างรวดเร็ว อย่างที่สามคือ generic ทุกครั้งที่มีการ instantiate generic จะกินทรัพยากร CPU ด้วย แต่จะขอแก้ต่างให้นิดหนึ่งว่า ฟีเจอร์พวกนี้ทำให้โค้ดที่ถ้าเขียนด้วย C อาจยาว 100,000 บรรทัด หรือ C++ 25,000 บรรทัด ลดเหลือเพียงหลักพันบรรทัดใน Rust ได้ อย่างไรก็ดี ก็เป็นความจริงว่าการใช้ฟีเจอร์เหล่านี้มากเกินไปทำให้ ecosystem ดูช้าลง ตัวอย่างเช่นบริษัทของเราใช้ async-graphql ซึ่งตัวไลบรารีเองยอดเยี่ยมมาก แต่พึ่งพา procedural macro อย่างหนัก มี issue ด้านประสิทธิภาพที่เปิดค้างมาหลายปีแล้ว และทุกครั้งที่เพิ่ม data type ก็รู้สึกได้ชัดว่าคอมไพเลอร์ช้าลง

    • สงสัยว่าทำไมเรามักเห็นการเขียนโปรแกรม C utility เล็ก ๆ เดิมขึ้นใหม่ด้วย Rust บ่อยมาก ดูบ่อยกว่ากรณีพอร์ตโปรแกรม C ขนาดใหญ่ระดับ 100,000 บรรทัดเสียอีก ที่เห็นบ่อยกลับเป็นโค้ดขนาดเล็กมาก อยากรู้ว่าความเร็วคอมไพล์ของโปรแกรมเล็ก ๆ ระหว่าง Rust กับ C เทียบกันอย่างไร ไม่ได้ถามเรื่องขนาดโปรแกรม แต่ถามเรื่องความเร็วคอมไพล์ อนึ่ง จากที่วัดล่าสุด toolchain ของ Rust compiler มีขนาดประมาณ 2 เท่าของ GCC ที่ผมใช้ 1. โปรแกรมเล็กระดับนี้ไม่ว่าภาษาไหนก็มีโอกาสซ่อนปัญหา memory safety ต่ำ และขนาดก็เล็กพอที่จะ audit ได้ง่าย สถานการณ์จึงต่างจากโปรแกรม C 100,000 บรรทัด
    • ทุกครั้งที่นิยาม type ใหม่ จะรู้สึกได้เลยว่าคอมไพเลอร์ช้าลง เท่าที่จำได้ ประสิทธิภาพของคอมไพเลอร์จะช้าลงแบบยกกำลังตาม “ความลึก” ของ type ในกรณีอย่าง GraphQL ที่มี nested type มาก ปัญหานี้ยิ่งรุนแรงเป็นพิเศษ
    • เพื่อรับมือกับปัญหาที่ macro ซึ่งขยายออกมาเป็นหลายสิบหรือหลายร้อยบรรทัดอาจทำให้ codebase โตแบบก้าวกระโดด ล่าสุดมีการเพิ่มการรองรับเครื่องมือวิเคราะห์แล้ว ดูข้อมูลที่เกี่ยวข้องได้ที่: https://nnethercote.github.io/2025/06/26/how-much-code-does-that-proc-macro-generate.html
  • Ryan Fleury สร้าง Epic RAD Debugger ด้วย C จำนวน 278,000 บรรทัด โดยใช้วิธี build แบบ unity (โค้ดทั้งหมดอยู่ในไฟล์เดียวเป็น compilation unit เดียว) และบน Windows ใช้เวลาเพียง 1.5 วินาทีในการ clean compile แค่กรณีนี้ก็แสดงให้เห็นแล้วว่าการคอมไพล์สามารถเร็วมากได้ เลยสงสัยว่าทำไม Rust หรือ Swift ถึงทำแบบเดียวกันไม่ได้

    • ยิ่งคอมไพเลอร์ต้องทำงานตอน build มากเท่าไร เวลา build ก็ยิ่งนานขึ้น Go สามารถทำเวลา build ต่ำกว่า 1 วินาทีได้แม้กับ codebase ขนาดใหญ่ เพราะมีทั้ง module system และ type system ที่เรียบง่ายซึ่งลดงานที่ต้องทำตอน build ให้เหลือน้อยที่สุด และโยนภาระส่วนใหญ่ไปให้ runtime GC แทน ในทางกลับกัน ถ้าต้องการ macro, type system ซับซ้อน, และความแข็งแกร่งระดับสูง เวลา build ก็หลีกเลี่ยงไม่ได้ที่จะยาวขึ้น
    • Rust เองก็ใช้หน่วย build ระดับทั้ง crate เช่นกัน และคอมไพเลอร์จะแบ่งเป็น LLVM IR ขนาดที่เหมาะสมให้เอง รวมทั้งปรับสมดุลระหว่างงานซ้ำซ้อนกับ incremental build ให้อัตโนมัติด้วย ถ้าวัดตามจำนวนบรรทัด source code แล้ว Rust มัก build ได้เร็วกว่า C++ อยู่บ่อย ๆ เพียงแต่โปรเจ็กต์ Rust มีลักษณะที่ต้องคอมไพล์ dependency ทั้งหมดด้วย
    • เหตุผลที่ Rust และ Swift คอมไพล์ช้ากว่าคอมไพเลอร์ C คือภาษาตัวมันเองต้องการการวิเคราะห์มากกว่ามาก ตัวอย่างเช่น borrow checker ของ Rust ไม่ได้มาแบบฟรี ๆ แค่การตรวจสอบตอนคอมไพล์ก็ใช้ทรัพยากรไม่น้อยแล้ว ที่ C เร็วก็เพราะแทบไม่ได้ตรวจอะไรเกินกว่าระดับไวยากรณ์พื้นฐานด้วยซ้ำ จริง ๆ แล้ว C ไม่เช็กด้วยซ้ำว่าการเรียก foo(int) ไปยัง foo(char*) เป็นการจับคู่ประหลาด
    • เคยคอมไพล์โปรเจ็กต์ C++ ระดับหลายหมื่นบรรทัดในยุค 2000s แล้ว build เสร็จภายในไม่ถึง 1 วินาทีแม้บนคอมพิวเตอร์เก่า ขณะเดียวกัน HELLO WORLD ที่ใช้แค่ Boost ก็ใช้เวลาหลายวินาที สุดท้ายแล้วความเร็ว build ไม่ได้ขึ้นกับแค่ภาษาและคอมไพเลอร์ แต่ขึ้นกับโครงสร้างโค้ดและฟีเจอร์ที่ใช้มากด้วย จะสร้าง DOOM ด้วย C macro ก็อาจทำได้ แต่คงไม่ได้เร็ว ส่วน Rust เองก็ออกแบบโครงสร้างให้ build เร็วได้เหมือนกัน
    • ที่ภาษาอย่าง C และ Go ซึ่งตั้งเป้าคอมไพล์เร็วจะคอมไพล์ได้เร็ว ไม่ใช่เรื่องน่าแปลกใจ สิ่งที่ยากจริง ๆ คือทำให้ semantics ของ Rust คอมไพล์ได้เร็ว ซึ่งปัญหานี้ก็อยู่ใน FAQ อย่างเป็นทางการของ Rust ด้วย
  • ดีใจมากที่ Go เลือกให้ความสำคัญกับความเร็วคอมไพล์มากกว่าการ optimize สำหรับงานเซิร์ฟเวอร์, networking, และ glue code การคอมไพล์ที่เร็วจริง ๆ สำคัญที่สุด ผมก็อยากได้ type safety ในระดับพอเหมาะ แต่ไม่อยากให้มันขัดขวางการทำ prototype แบบหลวม ๆ การมี GC ก็สะดวกด้วย ผมคิดว่าหลังจาก Google ผ่านประสบการณ์การพัฒนาขนาดใหญ่มาแล้ว ก็สรุปได้ว่า type ที่เรียบง่าย, GC, และการคอมไพล์ที่เร็วมาก สำคัญกว่าความเร็วตอนรันหรือความสมบูรณ์ของ semantics เสียอีก แค่มองกรณีซอฟต์แวร์ networking และ infrastructure ขนาดใหญ่ที่สร้างด้วย Go ก็เห็นได้เลยว่าการเลือกนี้แม่นมาก แน่นอนว่าในสภาพแวดล้อมที่ยอมรับ GC ไม่ได้ หรือที่ความถูกต้องสมบูรณ์สำคัญกว่า ก็อาจไม่ใช้ Go แต่กับสภาพงานของผม การเลือกของ Go เหมาะที่สุด

    • ผมก็ชอบ Go แต่ไม่คิดว่าภาษานี้เป็นผลผลิตจากปัญญารวมหมู่ระดับสุดยอดขององค์กรอย่าง Google ถ้ามันซึมซับประสบการณ์ของ Google มาจริง อย่างน้อยก็คงมีฟีเจอร์อย่างการกำจัด null pointer exception แบบ static ไปแล้ว ตรงกันข้าม มันดูเหมือนผลลัพธ์จากนักพัฒนาบางคนใน Google ที่สร้างภาษาที่ตัวเองอยากได้มากกว่า
    • แม้ Go จะมีข้อดีอย่างคอมไพล์เร็ว, type system ระดับพอเหมาะ, และ GC แต่ในเชิงพื้นที่การออกแบบ Java ก็ยืนอยู่จุดคล้ายกันอยู่ก่อนแล้ว ดูเหมือนแรงผลักหลักที่ทำให้ Go เกิดขึ้นจะมาจากความต้องการสร้างสรรค์มากกว่า และสุดท้ายมันก็เหมือนถูกดูดซับโดยฐานผู้ใช้ภาษาสคริปต์ (Python/Ruby/JS) มากกว่ากลุ่มเป้าหมายดั้งเดิมอย่างเซิร์ฟเวอร์ฝั่ง C/C++/Java ผู้ใช้สายสคริปต์ต้องการเพียง type system ที่ง่ายและเร็ว ส่วน Java ก็ดูเก่าและไม่สนุกเกินไปอยู่แล้ว เอาจริง ๆ Java ก็ไม่มีที่ว่างใหม่ในโลกเซิร์ฟเวอร์/คอนเฟอเรนซ์/ไลบรารีด้วย
    • มีคนเล่าว่านักพัฒนาของ Google ออกแบบ Go ระหว่างรอโปรเจ็กต์ C++ คอมไพล์
    • อยากถามว่า "obnoxious type" คืออะไร type ก็มีแค่แสดงข้อมูลได้ถูกต้องหรือไม่ถูกต้องเท่านั้น และในทางปฏิบัติ ไม่ว่าภาษาไหนก็สามารถบังคับให้ type checker เงียบได้ทั้งนั้น
    • Go เป็นภาษาที่ตรงกับเป้าหมายการออกแบบและการใช้งานจริงอย่างมาก ความเสี่ยงใหญ่ที่สุดคือการประมวลผลขนานและการแชร์ mutable state ผ่าน channel ซึ่งอาจก่อให้เกิดบั๊กที่ละเอียดอ่อนหรือเปราะบางได้ แต่โดยปกติผู้ใช้ส่วนใหญ่ไม่ได้ใช้แพตเทิร์นแบบนั้น ผมใช้ Rust เพราะงานของผมคือการเค้น algorithm ที่ช้าให้ไปรันบน hardware ที่ช้าให้ได้มากที่สุด ทำให้เกิดปัญหาที่การขนานงานขนาดใหญ่แทบเป็นไปไม่ได้อย่างละเอียดอ่อน
  • ผมไม่เข้าใจข้ออ้างที่ว่าการติดตั้งแบบ single static binary ง่ายกว่าการจัดการ container

    • ดูเหมือนจะยังไม่เข้าใจชัดว่า docker ทำอะไรจริง ๆ เช่นมีการบอกว่า "ถ้าปล่อยด้วย docker image ก็ต้อง build ใหม่ทั้งก้อนทุกครั้ง" แต่ในสภาพแวดล้อม build/deploy ภายในองค์กร เรื่องนี้อาจไม่ใช่ปัญหาเลย ถ้าใช้ส่วนตัว ก็สามารถใส่ไฟล์ที่ build จาก local ลงใน container ได้โดยยังรักษาความสะดวกในการพัฒนาไว้ได้เหมือนเดิม แค่ต้องระวัง path ที่ทิ้งร่องรอยของสภาพแวดล้อม build เท่านั้น สำหรับ CI/CD หรือโปรเจ็กต์ทีม จุดสำคัญคือการรับประกันว่าสามารถสร้าง build ได้ใหม่จากศูนย์ไม่ว่าที่ไหน แต่กับงานส่วนตัวไม่จำเป็นต้องเข้มงวดขนาดนั้น
    • ในต้นฉบับ เป้าหมายไม่ใช่การทำให้ง่ายขึ้น แต่คือการทำให้ทันสมัย กล่าวคือ "ในช่วง 10 ปีที่ผ่านมา ซอฟต์แวร์ส่วนใหญ่ใช้การ deploy แบบ container เป็นมาตรฐาน ดังนั้นเว็บไซต์ของฉันก็จะ deploy ด้วย container อย่าง docker, kubernetes เช่นกัน" container มีข้อดีหลายอย่าง เช่น process isolation, security, logging ที่เป็นมาตรฐาน, และ scalability แนวนอน
  • บนโน้ตบุ๊กของผม (Mac M4 Pro) การคอมไพล์ Deno ทั้งโปรเจ็กต์ (โปรเจ็กต์ Rust ขนาดใหญ่) ใช้เวลา 2 นาที ถ้าวัดเป็นคำสั่ง debug ใช้ประมาณ 1 นาที 54 วินาที ส่วน release ใช้ประมาณ 8 นาที 17 วินาที ตัวเลขนี้วัดโดยไม่ใช้งาน incremental compile จริง ๆ แล้ว build สำหรับ deploy ก็ไปรันบนระบบ CI/CD อยู่แล้ว จึงไม่จำเป็นต้องนั่งรอเอง

  • แล้วเรื่อง Cranelift อยู่ตรงไหน? ผมเองตอนพัฒนาเกมด้วย Rust เกือบจะเลิกไปแล้วเพราะเวลา compile นานเกินไป พอไปหาข้อมูลก็พบว่า LLVM ช้าไม่ว่า optimization level จะเป็นเท่าไร ซึ่งเป็นสิ่งที่นักพัฒนาภาษา Jai ชี้ให้เห็นมาตลอด ผมเคยเจอว่าพอใช้ Cranelift แล้วเวลา build ลดจาก 16 วินาทีเหลือ 4 วินาที ต้องยกนิ้วให้ทีม Cranelift จริง ๆ!

    • ตอนงาน Bevy game jam ล่าสุด ผมใช้เครื่องมือชื่อ 'subsecond' จากชุมชน Dioxus ซึ่งทำ hot reload ของระบบได้ต่ำกว่า 1 วินาทีสมชื่อ ช่วยเรื่องทำ UI prototype มาก https://github.com/TheBevyFlock/bevy_simple_subsecond_system
    • เท่าที่รู้ ทีม zig ก็พยายามทำคอมไพเลอร์ (backend) ของตัวเองโดยไม่ใช้ LLVM เพื่อให้เวลา build เร็วมากเช่นกัน
    • จำได้ว่าเมื่อก่อน Cranelift ยังไม่รองรับ macOS aarch64 แต่เพิ่งมารู้ว่าตอนนี้รองรับแล้ว
    • แค่ build 16 วินาทีถึงกับเกือบเลิกใช้ Rust เลยเหรอ ฟังดูเกินไปหน่อยไหม?
  • ผมไม่คิดว่า Rust ช้าเลย เมื่อเทียบกับภาษาระดับเดียวกันก็ถือว่าเร็วพอ และถ้าเทียบกับการคอมไพล์ C++/Scala ที่กินเวลา 15 นาที ก็เร็วกว่ามาก

    • เห็นด้วย ผมเองไม่เคยรู้สึกว่า Rust build น่ารำคาญเป็นพิเศษเลย อาจเป็นเพราะภาพจำแย่ ๆ จากยุคแรก ๆ ยังคงติดอยู่ คนเลยยังให้ความเห็นแบบนี้
    • การใช้หน่วยความจำตอนคอมไพล์สูงมากเมื่อเทียบกับ C/C++ ถ้าผมจะคอมไพล์โปรเจ็กต์ Rust ใหญ่ ๆ บน VM สำหรับเดโมใน YouTube ต้องมี RAM เกิน 8GB ถึงจะไหว กับ C/C++ ไม่เคยต้องกังวลเรื่องนี้
    • เนื่องจาก template ของ C++ นั้น Turing-complete การเอาเวลา build มาเทียบกันเฉย ๆ โดยไม่คำนึงถึงสไตล์โค้ดที่ใช้จริง จึงไม่มีความหมายมากนัก
  • ในฐานะอดีตนักพัฒนา C++ ผมไม่ค่อยเข้าใจคำกล่าวอ้างที่ว่า Rust build ช้า

    • ก็เพราะอย่างนั้นจึงมักมีคนบอกว่า Rust เจาะกลุ่มนักพัฒนา C++ นักพัฒนาที่ผ่าน C++ มาเยอะ ๆ มักมี Stockholm syndrome กับความไม่สะดวกของเครื่องมืออยู่แล้ว
    • ถึงจะเร็วกว่ C++ ก็ยังอาจช้าในเชิงค่าสัมบูรณ์ได้ ทุกคนก็รู้กันดีว่า build ของ C++ มีชื่อเสียระดับหนักมาก Rust ไม่ได้มีปัญหาเชิงโครงสร้างของภาษาแบบนั้น คนจึงคาดหวังสูงกว่า
    • ผมรู้สึกว่านี่เป็นตัวอย่างคลาสสิกของการเพิ่มฟีเจอร์ใหม่เรื่อย ๆ แต่ไม่ค่อยรับฟังผู้ใช้จริงและแก้ปัญหาให้ดีนัก
    • ขั้นตอนการคอมไพล์ของ C มีน้อยและเรียบง่ายจึงเร็ว แต่สำหรับ C++ ผมรู้สึกว่าการใช้ template กลับทำลายงาน encapsulation ไปเกือบหมด แค่เปลี่ยน template header เดียว ก็เหมือนทั้งโปรเจ็กต์ 98% ได้รับผลกระทบหมด
  • incremental compile ทรงพลังมาก หลัง build ครั้งแรกแล้ว สามารถตรึง snapshot ของ incremental cache ไว้ และถ้าไม่มีอะไรเปลี่ยนก็เอาไปใช้ build/deploy ต่อได้อย่างรวดเร็ว เข้ากันได้ดีกับ docker ด้วย นอกจากการเปลี่ยนเวอร์ชันคอมไพเลอร์หรืออัปเดตเว็บไซต์ขนาดใหญ่แล้ว ปกติจะไม่ไปแตะ layer ของ image ถ้ามีแค่โค้ดเปลี่ยน ก็สามารถตั้งค่าไม่ให้ layer นั้นถูก rebuild ได้อย่างมีประสิทธิภาพ

    • incremental artifact ของโปรเจ็กต์ผมใหญ่เกิน 150GB แล้ว ตอนที่เคยใช้ docker image ที่ใหญ่ขนาดนี้ มันก่อปัญหาใหญ่จริง ๆ หลายอย่าง
  • เวลา build หน้าโฮมเพจของผมคือ 73ms ส่วน static site generator ใช้เวลา recompile แค่ 17ms การรัน generator จริง ๆ ใช้เพียง 56ms เท่านั้น แนบ zig build log มาด้วย

    • ดูเหมือนว่าในโพสต์เกี่ยวกับ C/C++ จะมีคอมเมนต์ว่า Rust ดีกว่าเสมอ ส่วนในโพสต์เกี่ยวกับ Rust ก็จะมีคอมเมนต์ว่า Zig ดีกว่าเสมอ (พอรู้ทีหลังว่าคอมเมนต์นี้เขียนโดยผู้พัฒนาหลักของ zig) ผมคิดว่าการประกาศศาสนาภาษาแบบนี้เป็นผลเสียต่อชุมชน และในความเป็นจริงมักสร้างแรงต้านมากกว่าจะดึงผู้ใช้ใหม่เข้ามา ถ้ารักภาษานั้นจริง การช่วยกดวัฒนธรรมแบบนี้น่าจะเป็นประโยชน์กว่า
    • แทนที่จะยกตัวเลขเวลา compile เดี่ยว ๆ มาอย่างเดียว น่าจะมีการอภิปรายหรือการตีความที่เกี่ยวกับประเด็นหลักของบทความต้นทางโดยตรงมากกว่านี้
    • เว็บไซต์ Rust ของผม (รวมทั้งเฟรมเวิร์กแบบ react-like และเว็บเซิร์ฟเวอร์ที่ใช้งานจริง) ก็ใช้เวลาราว 1.25 วินาทีเมื่อทำ incremental build ด้วย cargo watch และถ้าใช้เครื่องมืออย่าง subsecond[0] ที่มีทั้ง incremental linking และ hotpatch ก็จะเร็วยิ่งขึ้น แม้จะไม่เร็วเท่า Zig แต่ก็ใกล้เคียงมาก ถ้า 331ms ที่พูดถึงข้างบนเป็น clean build จริง ๆ (ไม่มี cache) ก็ยังเร็วกว่า clean build ของเว็บไซต์ผมที่ใช้ 12 วินาทีมาก [0]: https://news.ycombinator.com/item?id=44369642
    • อยากถาม @AndyKelley มาก ๆ ว่าคิดว่าเหตุผลชี้ขาดที่ทำให้ zig คอมไพล์เร็วมาก แต่ Rust กับ Swift ช้าตลอด คืออะไร
    • Zig ไม่ได้รับประกัน memory safety ใช่ไหม?