ความเร็วการบิลด์ของ Zig กำลังเร็วขึ้น
(mitchellh.com)- การเปิดตัว Zig 0.15.1 ทำให้ ความเร็วในการคอมไพล์ ดีขึ้นอย่างมากเมื่อเทียบกับเวอร์ชันก่อนหน้า
- จากการวัดเวลาบิลด์จริงใน โปรเจ็กต์ Ghostty พบว่า เวลาในการทำงานโดยรวมสั้นลง
- แม้ยังคง ใช้ LLVM บางส่วน อยู่ แต่ก็มีความคาดหวังสูงว่าจะเร็วขึ้นได้อีกเมื่อใช้แบ็กเอนด์ของตัวเอง
- การคอมไพล์แบบ incremental ยังไม่ถูกทำให้สมบูรณ์ทั้งหมด แต่ก็เริ่มเห็นข้อได้เปรียบด้านประสิทธิภาพแม้ในงานบิลด์บางส่วน
- ในอนาคตมีความเป็นไปได้สูงที่จะได้สภาพแวดล้อมการบิลด์ที่เร็วขึ้น และ ประสบการณ์การพัฒนาที่ดีขึ้น
ภาพรวม
จากคำพูดของ Andrew Kelley ที่ว่า "คอมไพเลอร์ช้าเกินไปจนทำให้เกิดบั๊ก" Zig จึงเดินหน้าปรับโครงสร้างหลายด้านตลอดหลายปีที่ผ่านมาโดยมีเป้าหมายคือ เวลาในการคอมไพล์ที่เร็วขึ้น
- ทีม Zig พยายาม ถอด LLVM ออก, พัฒนา แบ็กเอนด์สร้างโค้ด ของตัวเอง, สร้าง ลิงเกอร์ ของตัวเอง และท้ายที่สุดคือทำให้ การคอมไพล์แบบ incremental เกิดขึ้นจริง
- ผลลัพธ์ของการพัฒนาระยะยาวนี้เริ่มปรากฏอย่างชัดเจนใน Zig 0.15.1 และมีการวัดพร้อมแชร์การเปลี่ยนแปลงของเวลาบิลด์ในโปรเจ็กต์จริงอย่าง Ghostty
ความเร็วในการคอมไพล์ Build Script
- Zig 0.14: 7 วินาที 167 มิลลิวินาที
- Zig 0.15: 1 วินาที 702 มิลลิวินาที
นี่คือเวลาบิลด์ของสคริปต์ build.zig เอง ซึ่งเป็นต้นทุนเริ่มต้นที่ต้องจ่ายทุกครั้งในสภาพแวดล้อมบิลด์ซอร์สใหม่
- ความถี่ในการคอมไพล์สคริปต์บิลด์ซ้ำยังคงอยู่ในระดับต่ำ แต่ส่งผลโดยตรงต่อประสบการณ์ของผู้ใช้ที่บิลด์โปรเจ็กต์ด้วยตัวเองเป็นครั้งแรก
การบิลด์ไบนารีเต็มแบบไม่ใช้แคช (Ghostty)
- Zig 0.14: 41 วินาที
- Zig 0.15: 32 วินาที
นี่คือเวลาบิลด์ไบนารีทั้งหมดรวมเวลาในการบิลด์สคริปต์บิลด์ด้วย
- Zig 0.15 ให้ผลด้านความเร็วเพิ่มขึ้นอีกราว 2 วินาที และยังเห็นความแตกต่างเริ่มต้นได้ชัดเจนแม้ดูจากเวลาแบบ wall-clock จริง
- ขณะนี้ แบ็กเอนด์ x86_64 ของตัวเอง ยังไม่ถูกใช้งานได้อย่างเต็มที่ และส่วนใหญ่ยังคงใช้ LLVM ต่อไป
- คาดว่าเมื่อ Ghostty บิลด์ด้วย แบ็กเอนด์ของตัวเอง ได้อย่างสมบูรณ์ในอนาคต เวลาจะลดลงต่ำกว่า 25 วินาทีได้ (ประมาณครึ่งหนึ่งของเดิม)
การบิลด์แบบ incremental (ไฟล์รันของ Ghostty)
- Zig 0.14: 19 วินาที
- Zig 0.15: 16 วินาที
นี่คือเวลาที่ใช้ในการบิลด์ใหม่หลังจากแก้ไขหนึ่งบรรทัดที่มีความหมาย (เพิ่ม log call ในโค้ด terminal emulation)
- เป็นการบิลด์บางส่วนในสถานะที่สคริปต์บิลด์และกราฟ dependency ถูกแคชไว้แล้ว
- แม้ ฟีเจอร์การคอมไพล์แบบ incremental จะยังไม่ถูกทำให้สมบูรณ์ แต่ก็เห็นการปรับปรุงด้านประสิทธิภาพได้ชัดเจนแล้ว
- หากตัดส่วนที่ยังใช้ LLVM ออก ก็มีโอกาสลดลงได้ถึงราว 12 วินาที
- หากมีการทำ incremental build ที่แท้จริงในอนาคต ก็อาจคาดหวังการบิลด์ระดับมิลลิวินาทีได้
การบิลด์แบบ incremental (libghostty-vt)
- Zig 0.14: 2 วินาที 884 มิลลิวินาที
- Zig 0.15: 975 มิลลิวินาที
เป็นการวัดเวลาที่ใช้บิลด์ใหม่บางส่วนเฉพาะ libghostty-vt หลังจากแก้ไขหนึ่งบรรทัด
libghostty-vtสามารถบิลด์ได้ทั้งหมดด้วย แบ็กเอนด์ x86_64 ของตัวเอง จึงสะท้อนการปรับปรุงของ Zig ได้โดยตรงโดยไม่ถูกรบกวนจาก LLVM- แม้ยังไม่ใช่ incremental compilation ก็ถือว่าการทำเวลาบิลด์ต่ำกว่า 1 วินาทีเป็นความก้าวหน้าที่สำคัญ
- ช่วยเพิ่มประสิทธิภาพในเวิร์กโฟลว์ของนักพัฒนาด้วย ประสบการณ์ฟีดแบ็กที่แทบจะทันที
- แบ็กเอนด์ x86_64 และ aarch64 กำลังเสถียรมากขึ้นเรื่อย ๆ และมีโอกาสถูกนำไปใช้กับ Ghostty ทั้งหมดภายในไม่กี่เดือน
สถานะปัจจุบันของการปรับปรุงความเร็วการบิลด์
- การบิลด์ Ghostty ด้วย Zig 0.15.1 เร็วขึ้นอย่างชัดเจนในทุกช่วงการวัด
- แม้แบ็กเอนด์ของตัวเองและการคอมไพล์แบบ incremental จะยังไม่เสร็จสมบูรณ์ แต่ผลงานในตอนนี้ก็น่าประทับใจเพียงพอแล้ว
- ในอีก 1-2 ปีข้างหน้า มีแนวโน้มว่าจะได้เห็นผลลัพธ์ด้านความเร็วที่ก้าวกระโดดยิ่งกว่านี้
- ทำให้รู้สึกได้ว่าการเลือก Zig เป็นทางเลือกที่สมเหตุสมผลในมุมมองของความเร็วในการบิลด์
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ตอนจบมัธยมปลายในปี 1995 เคยได้สัมผัสความเร็วคอมไพล์ที่เร็วมากพอๆ กับตอนรีคอมไพล์ "Borland Pascal version Turbo Vision for DOS" บน Intel 486
Turbo Vision คือเฟรมเวิร์กหน้าต่างแบบ TUI ที่ใช้พัฒนา IDE ของ Borland Pascal และ C++
พูดได้ว่าเป็นโหมดตัวอักษรที่เหมือนทำ JetBrains IDE ขนาด 10MB แทน 1000MB
วิกิพีเดียของ Turbo Vision
LLVM เป็นเหมือนกับดักแบบหนึ่ง
มันทำให้ bootstrap ได้เร็วมาก และได้ optimization pass สารพัดพร้อมรองรับหลายแพลตฟอร์มแบบฟรีๆ แต่ก็แลกกับการสูญเสียความสามารถในการจูนประสิทธิภาพของขั้นตอน optimization ท้ายๆ หรือขั้นตอน linking อย่างละเอียด
คิดว่า Cranelift น่าจะถูกเปิดใช้ใน Rust เร็วๆ นี้
อย่างไรก็ตาม Rust ก็ได้สถานะอย่างทุกวันนี้เพราะตอนเริ่มต้นเลือก LLVM
Go ตัดสินใจมานานแล้วว่าจะไม่ฝาก code generation และการลิงก์ไว้กับภายนอก แต่จัดการเองทั้งหมด และก็ได้ประโยชน์จากการตัดสินใจนั้นเต็มๆ
เห็นด้วยได้ยากกับคำกล่าวที่ว่า LLVM เป็นกับดัก และ Rust นี่แหละเป็นตัวอย่างที่ดี
ในความเป็นจริง ส่วนที่ generate code ด้วย LLVM กินสัดส่วนเล็กมากในคอมไพเลอร์ และถ้าจะเปลี่ยนก็สลับไปใช้ codegen_cranelift หรือ codegen_gcc ได้
การผูกติดกับ SIMD vendor intrinsics นั้นเป็นปัญหาแบบ lock-in จริง แต่เป็นปัญหาจากโครงสร้างของภาษา
สำหรับภาษาส่วนใหญ่ การเริ่มต้นด้วย LLVM backend ถือว่าสมเหตุสมผล
ภาษาที่คล้าย C/C++ มักได้ optimization ที่ดีจาก pipeline พื้นฐานของ LLVM อยู่แล้ว แต่ยิ่งภาษามีลักษณะต่างออกไปมากเท่าไร ก็ยิ่งต้องเขียน optimization pipeline เอง
กรณีอย่าง Go ที่รวม backend ของตัวเองมาตั้งแต่แรกก็ดูประสบความสำเร็จ แต่ไม่ได้เป็นจุดต่างพิเศษอะไร และการทำเองก็มีต้นทุนค่าเสียโอกาสไม่น้อย
คอมไพเลอร์ของ Go และ Ocaml เร็วมากจริงๆ
พวกมันสร้างไลบรารีของตัวเองอย่างจริงจังมาตั้งแต่แรก และตอนนี้ก็แทบไม่มีอะไรให้เสียในแง่ความเร็วแล้ว
ต่อไปไม่อยากทำงานในสภาพแวดล้อมที่คอมไพล์เกิน 1 นาทีอีก
อยากให้แต่ละโปรเจ็กต์มีคอมไพเลอร์สำหรับ
devโดยเฉพาะ แล้วค่อยใช้ของหนักอย่าง llvm เฉพาะตอน final buildถ้าภาษาที่อิง LLVM ตั้งเป้าแทนที่ C++ สุดท้ายก็ยังต้องพึ่ง C++ อยู่ดี
ภาษาควร bootstrap ได้ด้วยตัวมันเอง
ช่วงแรกอาจมีเครื่องมือสะดวกๆ ช่วยได้ แต่ก็เป็นแค่ตัวประหยัดเวลา ไม่ใช่ของจำเป็น
เห็นด้วยอย่างยิ่งกับการตัดสินใจของทีม Go
หวังว่า Cranelift จะเติบโตต่อไปเรื่อยๆ
LLVM ทุกวันนี้มี fork จากผู้ผลิต CPU แยกออกมามากมาย และแต่ละเจ้าก็เอาการปรับปรุงตาม CPU ไปมัดรวมไว้ในแพ็กเกจปิด
ถ้าใช้ frontend ของภาษาอื่น หรือเจอบั๊กคอมไพเลอร์ สถานการณ์จะลำบากมาก
มีการตั้งคำถามว่าถ้าภาษาต่างๆ ย้ายไปใช้ backend อื่นได้อย่างอิสระ แล้วจะเรียกว่าเป็นกับดักได้อย่างไร
ถ้าความเร็วคอมไพล์รบกวนการพัฒนา ทำไมไม่สร้าง interpreter ไปเลย
ความเร็วรันกับความเร็วคอมไพล์เป็นเรื่องที่โดยเนื้อแท้แล้วแยกจากกันได้
ถ้าใช้ interpreter ก็ยังสร้างเครื่องมือพัฒนาเพิ่มอย่าง code instrumentation หรือการควบคุม runtime ได้ง่ายด้วย
แม้จะมีบางกรณีน้อยมากที่ต้องดีบักเฉพาะ RELEASE binary ที่ optimize แล้ว แต่ส่วนใหญ่ใช้ interpreter หรือ DEBUG build ก็พอ
เคยได้ยินมาว่า Rust จัดลำดับเป็น safety, performance, usability ส่วน Zig เป็น performance, usability, safety
ในมุมนี้ การทำให้ build เร็วขึ้นฟังดูสมเหตุสมผล แต่ interpreter เป็นทางเลือกที่เหมาะกว่าเมื่อให้ความสำคัญกับ usability ก่อน
ชอบแนวทางของ Julia
ในสภาพแวดล้อมแบบ interactive เต็มรูปแบบ interpreter จะคอมไพล์โค้ดแล้วรันทันที
สภาพแวดล้อม Common Lisp อย่าง SBCL ก็คล้ายกัน
ถ้ามองแบบสุดโต่ง ความเร็วรันกับความเร็วคอมไพล์เป็นอิสระจากกัน
และมี "พื้นที่ประนีประนอม" อยู่ระหว่างนั้น ซึ่งทำให้คอมไพเลอร์เร็วขึ้นได้โดยไม่ต้องเสียประสิทธิภาพของไฟล์รัน
มีคนยืนยันว่าแวดวงเกมไม่ได้เป็นกรณีพิเศษอะไร
จนถึงตอนนี้ ความเร็วคอมไพเลอร์ที่ดีที่สุดคือ TCC (ผลงานของ Fabrice Bellard)
แม้ไม่มี multithread หรือ optimization ซับซ้อนก็ยังเร็วแบบทิ้งห่าง
ถึงจะใช้ Clang สำหรับรีลีส แต่ประสิทธิภาพ code generation ของ TCC ก็ไม่ได้แย่
คิดว่า Delphi ให้ทั้งความสามารถในการแสดงออกที่มากกว่า ปลอดภัยกว่า และยังคอมไพล์เร็วมากด้วย
คอมไพเลอร์ Go ก็เร็วพอตัวเหมือนกัน
เคยมองว่า DMD เป็น "gold standard" เพราะคอมไพล์ได้ทั้ง C และ D แล้วยังเร็ว
อยากให้ TCC รองรับ C23
ไม่มีอะไรที่เป็น "gold standard ที่แท้จริง" เพียงหนึ่งเดียว
vlang ก็รีคอมไพล์เสร็จในไม่กี่วินาที และคอมไพเลอร์ของ Go ก็เร็วมากเช่นกัน
ยังมีเทคนิคอย่าง build caching ที่ช่วยหลีกเลี่ยงการรีคอมไพล์ไปเลยด้วย ดังนั้นไม่ใช่สิ่งที่ใครผูกขาดได้คนเดียว
ตอน build แอปด้วย zig ประทับใจกับ incremental build มาก
สามารถ build single static binary ที่ใช้ไลบรารีหลากหลายอย่าง SQLite, luau ฯลฯ ได้เร็วพอๆ กับ Go
แต่คอมไพเลอร์แบบ self-hosted ก็ยังมีบั๊กเหลืออยู่พอสมควร
เช่น SQLite ยังต้องใช้ llvm และดูประเด็นที่เกี่ยวข้องได้ที่นี่
สงสัยว่า Zig จะทำงานร่วมกับระบบ build อย่าง Bazel หรือ Buck2 ได้ดีแค่ไหน
สคริปต์ build ของ Zig เป็นแบบ Turing-complete เลยกังวลว่าในระบบพวกนี้การทำ caching และ build automation อาจไม่ง่าย
คล้ายเหตุผลที่ใน Rust ไลบรารีที่ใช้ได้โดยไม่ต้องมี build.rs มักเป็นที่ชอบมากกว่า
เลยสงสัยว่าไลบรารีของ Zig มีการทำ custom build เยอะไหม
สคริปต์ build ของ Zig เป็นทางเลือกทั้งหมด
ต่อให้ไม่มี build.zig ก็ยัง build/run ไฟล์ซอร์สเดี่ยวได้ทันที
สามารถเอา Zig ไปผูกเข้ากับ workflow ไหนก็ได้ที่เดิมใช้ GCC หรือ Clang
อีกอย่าง Zig ยังทำหน้าที่เป็นตัวแทน C compiler ได้ด้วย
บทความที่เกี่ยวข้อง
มีการแนะนำ rules_zig เป็นตัวอย่างการเชื่อม Bazel กับ Zig
โปรเจ็กต์จริงอย่าง ZML ก็ใช้อยู่
โปรเจ็กต์ ZML
สงสัยว่าการคอมไพล์และ code generation ของ Zig เทียบกับ TPDE เป็นอย่างไร
แม้จะบอกว่าเร็วกว่า LLVM -O0 ราว 10~20 เท่า แต่ก็ดูเหมือนยังมีข้อจำกัด
มองว่ากลยุทธ์ของ Zig กล้าหาญมาก
แต่ก็ยังสงสัยว่าการใช้ LLVM backend ต่อไปเป็นทางที่ถูกหรือไม่
LLVM แข็งแกร่งทั้งด้านความเร็วคอมไพล์และการรองรับหลายแพลตฟอร์ม แต่ถ้าเป็นการสร้าง machine code ที่ดีที่สุดก็ยังไม่มีใครเทียบได้
ใช้ LLVM backend เฉพาะ Release build ส่วน debug build จะใช้วิธี self-hosted เป็นค่าเริ่มต้นในแพลตฟอร์มที่รองรับ
เพราะ debug build ต้องถูกคอมไพล์ซ้ำหลายรอบในกระบวนการทดสอบจริง แนวทางนี้จึงสมเหตุสมผลกว่า
ไม่ค่อยอินกับความหมกมุ่นเรื่องประสิทธิภาพคอมไพเลอร์
สุดท้ายมันคือ trade-off ระหว่าง optimization pass ต่างๆ เช่น inlining, dead code elimination ฯลฯ กับเวลาคอมไพล์
คอมไพเลอร์ที่ไม่มี optimization จะเร็วขึ้นแบบเชิงเส้นเท่านั้น และถ้าจะ optimize มากกว่านั้น เวลาที่ใช้ก็ย่อมเพิ่มสูงอยู่เสมอ
เรื่อง "แอสเซมบลีที่ยอดเยี่ยม" ไม่ได้เป็นประเด็นสำคัญจริงๆ
ตาม Proebsting's Law ความก้าวหน้าของเทคนิคคอมไพเลอร์ช้ากว่าการเพิ่มขึ้นของประสิทธิภาพเครื่องมาก
สรุปคือ optimization ที่ง่ายและเร็วก็เพียงพอในทางปฏิบัติแล้ว
LLVM เองก็ไม่ได้ optimize ได้แบบสัมบูรณ์ และเมื่อเทียบกับเวลาในการคอมไพล์ก็ชนเพดานได้เร็ว
กำลังทำ Java library ที่ห่อ C library ด้วย JNI แล้วรู้สึกว่าการ build dynamic library แยกตามแพลตฟอร์มยุ่งยากเกินไป เลยกำลังมอง Zig สำหรับการ build หลายแพลตฟอร์ม
ถ้าเป็นแค่ shim ง่ายๆ บน Java รุ่นใหม่ การใช้ Panama กับ jextract น่าจะดีกว่า
Zig ฝังทั้ง header, ซอร์สของ libc และ LLVM ของตัวเองมาให้ครบ และทำ cross compile ได้ง่ายมาก
ง่ายชนิดที่แทบไม่ต้องคิดอะไรเลย
สงสัยว่าทำไม Ghostty ถึงยัง build ไม่ได้ด้วย self-hosted x86_64 backend ในตอนนี้
เป็นเพราะ Zig compiler crash จากบั๊ก
ยังเป็นเทคโนโลยีใหม่จึงซับซ้อน แต่คงแก้ได้ในไม่ช้า
ผู้ใช้ Ghostty ส่วนใหญ่อยู่บนแพลตฟอร์ม aarch64