บันทึกความคืบหน้าการเขียนคอมไพเลอร์ Roc ใหม่จาก Rust→Zig
(rtfeldman.com)- เพื่อ แก้ไขข้อบกพร่องเชิงโครงสร้าง ของอิมพลีเมนเทชันเดิม จึงเขียนใหม่จาก Rust 300,000 บรรทัดเป็น Zig และใช้เวลา 487 วันกว่าจะบรรลุความเท่าเทียมด้านฟังก์ชัน โดยตั้งเป้าออกรีลีสหมายเลขทางการครั้งแรก 0.1.0 ในช่วงครึ่งหลังของปีนี้
- คอมไพเลอร์ตัวใหม่รองรับ hot code loading และ cross compilation แบบทำซ้ำได้, การทำ string interpolation ภายใน pattern matching, การตัด heap allocation ออกจาก HTTP routing และยังลดขนาด wasm ของ
Rocci Birdลงเหลือ 31KB ซึ่งน้อยกว่าครึ่งหนึ่ง - เหตุผลหลักที่เลือก Zig คือเวลา build, การควบคุม allocator และการจัดวางข้อมูลอย่างละเอียด, ecosystem ที่เหมาะกับการพัฒนาคอมไพเลอร์, และการตรวจสอบโค้ดที่ไม่ปลอดภัยต่อหน่วยความจำ โดย incremental build ของ Zig 0.17.0 สามารถ build ใหม่ราว 460,000 บรรทัดได้ใน 35ms
- จากการจัดประเภทบั๊กจริง คอมไพเลอร์ Rust มีกรณี memory corruption 21 รายการ ส่วนคอมไเลอร์ Zig มี 10 รายการ แต่ส่วนใหญ่เกิดจากการสร้างโค้ดผิดพลาด และข้อผิดพลาดด้าน memory safety ของตัวคอมไพเลอร์ Zig เองมีเพียง use-after-free 2 กรณี ที่ทำให้ชื่อไฟล์เสียหาย
- Zig เข้ากันได้ดีกับโครงสร้างข้อมูลแบบไม่มีพอยน์เตอร์, การ deserialize แบบไม่ต้อง parse, และการนำ LLVM bitcode serializer กลับมาใช้ซ้ำ แต่ในด้านการคืนหน่วยความจำอัตโนมัติของการทดสอบ, polymorphism, ฟิลด์ของ struct แบบ private, การตรวจจับ dead code, และความเข้ากันได้ระหว่างรีลีส ประสบการณ์การพัฒนาใน Rust ยังดีกว่า
การเขียนใหม่ที่บรรลุความเท่าเทียมด้านฟังก์ชัน
- ทีมคอมไพเลอร์ Roc ใช้เวลาราว 1 ปีครึ่งในการเขียนใหม่จาก Rust 300,000 บรรทัด เป็น Zig และบรรลุความเท่าเทียมด้านฟังก์ชันกับคอมไพเลอร์เดิม
- ความเท่าเทียมด้านฟังก์ชันเป็นหมุดหมายสำคัญ แต่ยังไม่ใช่รีลีสทางการ โดย 0.1.0 ของคอมไพเลอร์ใหม่ตั้งเป้าไว้ในช่วงครึ่งหลังของปีนี้
- ตอนนี้สามารถ build เกม WASM-4 ปี 2024 Rocci Bird ด้วยคอมไพเลอร์ตัวใหม่ได้แล้ว
- ตัวเกมทั้งหมดมีโค้ด Roc ไม่ถึง 1,000 บรรทัด
- ซอร์สที่อัปเดตแล้ว กระชับกว่าต้นฉบับ
- ไบนารี wasm ที่
roc build --opt=sizeสร้างขึ้นมีขนาด 31KB เล็กกว่าครึ่งหนึ่งของผลลัพธ์จากคอมไพเลอร์เดิม
- บนหน้าเว็บไซต์หลัก ผู้ใช้สามารถเขียนและรันโปรแกรม Roc พื้นฐานในเบราว์เซอร์ได้ด้วยคอมไพเลอร์ WebAssembly ขนาด 2.5MB
- การเขียนใหม่นี้ใช้เวลา 487 วัน ซึ่งนานกว่า 11 วันที่ Bun ย้าย Zig ราว 500,000 บรรทัดไปเป็น Rust อยู่ 476 วัน
- Bun เป็นการพอร์ตโดยตรง แต่ของ Roc ใกล้เคียงกับการเขียนใหม่มากกว่า เพราะมีการเปลี่ยนโครงสร้างและฟีเจอร์ของคอมไพเลอร์หลายส่วนอย่างมาก
- ดังนั้นจึงยากที่จะเปรียบเทียบขนาดหรือระยะเวลาของทั้งสองโค้ดเบสโดยอิงแค่ Rust กับ Zig โดยตรง
ประสบการณ์การพัฒนาที่คอมไพเลอร์ใหม่มอบให้
-
Hot code loading และ cross compilation
- เมื่อรันเซิร์ฟเวอร์ด้วย
roc server.rocแล้วแก้ไขโค้ด โค้ดใหม่จะถูกนำมาใช้โดยอัตโนมัติ ตั้งแต่คำขอถัดไป - นี่เป็นพฤติกรรมที่พบได้ทั่วไปในภาษา interpreter อย่าง Python แต่ไม่ค่อยพบในภาษาแบบคอมไพล์ประสิทธิภาพสูงอย่าง Roc
- ทำงานแบบเดียวกันนี้ได้ไม่ใช่แค่กับเว็บเซิร์ฟเวอร์ แต่รวมถึงเกม 2D แบบง่ายด้วย
- เมื่อต้องการ deploy,
roc build server.rocจะสร้าง standalone binary ที่ใช้ LLVM optimization - สามารถ cross-compile ไบนารีแบบ static สำหรับ Alpine Linux ได้ด้วย
roc build --target=x64musl - หากป้อน source bytes ชุดเดียวกัน ก็จะได้ output bytes ชุดเดียวกัน จากระบบ build ใดก็ตาม รวมถึง Mac
- เมื่อรันเซิร์ฟเวอร์ด้วย
-
String interpolation ภายใน pattern matching
- รองรับ pattern matching ที่มี string interpolation อย่าง
"/users/${id}" - คอมไพเลอร์จัดการโดยตรงแทนการ parse template string ระหว่างรันไทม์แบบ routing สไตล์ Express
- สามารถจับคู่ HTTP method และ path ไปพร้อมกันได้ และยังแตกแขนงด้วย pattern สำหรับ nested path หรือค่าปริยายได้
- โค้ดตัวอย่างสำหรับจัดการ HTTP request ทั้งชุด ไม่ทำ heap allocation เลยแม้แต่น้อย พร้อมทั้งตรวจสอบ type safety ระหว่างคอมไพล์
- ใช้การรัน pure function ระหว่างคอมไพล์เพื่อทำ HTTP request routing แบบไม่ต้องจัดสรรหน่วยความจำ และสามารถลอง syntax นี้ได้ในคอมไพเลอร์ WebAssembly บนหน้าเว็บไซต์หลัก
- รองรับ pattern matching ที่มี string interpolation อย่าง
เหตุผลที่เขียนใหม่ตั้งแต่ต้น
- Roc ต่างจาก Rust·C·Zig ตรงที่ไม่ใช่ภาษา systems language และใช้ การจัดการหน่วยความจำอัตโนมัติแบบอิง reference counting
- เพื่อหลีกเลี่ยงการหยุดชั่วคราวของ tracing garbage collector
- ใช้ การปรับแต่ง Perceus และการเปลี่ยนแปลงแบบฉวยโอกาสสไตล์ Koka
- แทนที่จะจัดสรร heap ให้ทุกการจับ closure แบบภาษาที่ไม่ใช่ systems language ทั่วไป จะใช้ polymorphic defunctionalization ผ่าน lambda set specialization
- Defunctionalization เปิดทางให้เกิดการปรับแต่งต่อเนื่องหลายอย่างเหมือน inlining ในภาษาเชิงฟังก์ชัน แต่การทำให้อิมพลีเมนเทชันเดิมถูกต้องแม่นยำนั้นยากมาก
- ผ่าน ต้นแบบ OCaml ของ Ayaz Hafiz จึงยืนยันได้ว่าปัญหาเป็น ข้อบกพร่องเชิงโครงสร้าง ที่พาดผ่านหลายขั้นของคอมไพเลอร์ และถ้าจะแก้ก็ต้องเขียนคอมไพเลอร์ส่วนใหญ่ใหม่
- ผู้มีส่วนร่วมคนอื่นก็มีแผนจะเขียนหลายส่วนใหม่ด้วยเหตุผลของตนเองอยู่แล้ว จึงเริ่มพิจารณาการเขียนใหม่ทั้งหมด แทนแนวทางเรือของเธเซอุสที่ค่อย ๆ แทนที่เกือบทุกส่วนแบบค่อยเป็นค่อยไป
- ในโครงการคอมไพเลอร์ที่ประสบความสำเร็จ มักมีกรณีที่เขียนใหม่ตั้งแต่ต้นเพื่อรองรับ self-hosting เป็นต้น และยังมีกรณี การเขียน TypeScript ใหม่ ที่ย้ายไปใช้ Go ซึ่งไม่ใช่ภาษาของตัวเอง
- แม้ Roc compiler จะมีนโยบายไม่ทำ self-hosting แต่ครั้งนี้ตัดสินใจว่าประโยชน์จากการเขียนใหม่มีมากกว่าต้นทุนที่ทราบอยู่แล้ว
เกณฑ์ที่ใช้เลือก Zig แทน Rust อีกครั้ง
- ทีมใช้ Zig กับฟังก์ชันพื้นฐานหลายอย่างของ standard library อยู่ก่อนแล้ว และภาษาระดับ systems ที่ทีมรู้จักดีพอมีแค่ Rust และ Zig จึงพิจารณาอย่างจริงจังเพียงสองภาษานี้
- แต่ละโปรเจกต์เหมาะกับภาษาต่างกัน และการที่ยังใช้ Rust กับงานอื่นต่อไปก็ไม่ได้ขัดแย้งกับการเลือก Zig สำหรับ Roc
-
เวลา build
- การ build ด้วย
cargoของ Rust ใช้เวลานาน แม้แต่ incremental build ก็ยังช้า และยิ่งโค้ดเบสใหญ่ก็ยิ่งเป็นความไม่สะดวกสำคัญ - ทีมคาดว่าหากย้ายไป Zig เวลา build จะเร็วขึ้นมาก
- การ build ด้วย
-
การควบคุมหน่วยความจำและการจัดวางข้อมูล
- ในแต่ละขั้นของคอมไพล์ มีการใช้ allocator หลายแบบ โดยเฉพาะ arena allocator และใช้การจัดวางแบบ struct-of-arrays (SoA) อย่างกว้างขวาง
- ecosystem ของ Rust โดยทั่วไปมักสมมติให้มี global allocator ตัวเดียว แต่ ecosystem ของ Zig ถือว่าการส่ง allocator แบบละเอียดเป็นค่าเริ่มต้น และยังรองรับ SoA ในมาตรฐานด้วย
-
ขอบเขตของ ecosystem ที่ต้องใช้
- แม้ ecosystem โดยรวมของ Rust จะใหญ่กว่า Zig แต่แพ็กเกจที่เกี่ยวข้องกับความต้องการเฉพาะของ Roc มีไม่มากนักทั้งสองฝั่ง
- ฟังก์ชันเฉพาะทางที่ต้องใช้ เช่น โค้ดที่สร้าง bitcode ได้รวดเร็วแทนการห่อ LLVM C++ library กลับมีให้ฝั่ง Zig มากกว่า
-
การรองรับโค้ดที่ไม่ปลอดภัยต่อหน่วยความจำ
- คอมไพเลอร์ Rust เดิม 300,000 บรรทัดมีการใช้
unsafeราว 1,200 จุด - เพื่อใช้เป็นตัวเทียบ คอมไพเลอร์ Rust เองใช้
unsafeราว 40,000 จุดในโค้ด 3.5 ล้านบรรทัด - ในคอมไพเลอร์ที่สร้าง machine code งานที่ไม่ปลอดภัยต่อหน่วยความจำเป็นส่วนสำคัญของงาน
- Rust มีโมเดลที่แยก
unsafeblock ซึ่งพบไม่บ่อยออกมาต่างหาก แล้วตรวจด้วย Miri หรือ Valgrind แต่สำหรับ Roc นั้นunsafeไม่ใช่สิ่งที่พบได้ยาก - Zig มีฟีเจอร์สำหรับทำให้โค้ดที่ไม่ปลอดภัยต่อหน่วยความจำทำงานได้อย่างถูกต้องมากกว่า และทีมก็ต้องการการสนับสนุนมากที่สุดในพื้นที่นี้โดยตรง
- คอมไพเลอร์ Rust เดิม 300,000 บรรทัดมีการใช้
ความปลอดภัยของหน่วยความจำที่ได้มาโดยไม่ต้องมี borrow checker
-
ขอบเขตการตรวจสอบของ Rust และ Zig
- ตามข้อมูลของ Microsoft ในปี 2019 ช่องโหว่ประมาณ 70% ที่ต้องออกอัปเดตความปลอดภัยในแต่ละปี เป็นปัญหาด้านความปลอดภัยของหน่วยความจำ
- หากแยกการจัดประเภทของปี 2018 ในข้อมูลดังกล่าวในมุมมองของ Rust และ Zig จะได้ดังนี้
- 83.6% เป็นการอ่าน·เขียนนอกขอบเขต, การ cast ที่ไม่ปลอดภัย, การอ่านค่าที่ยังไม่ถูกกำหนดค่าเริ่มต้น, stack overflow, และปัญหาที่ไม่ใช่ความปลอดภัยของหน่วยความจำ ซึ่งเป็นหมวดที่ไม่ได้รับผลจากการเลือกระหว่าง Rust กับ Zig
- 16.4% เป็น use-after-free ซึ่งเป็นหมวดที่สามารถตรวจจับได้ด้วย borrow checker ของ Rust,
ReleaseSafeของ Zig, และการตรวจสอบแบบ Fil-C ReleaseSafeของ Zig จะทำให้เกิด panic ระหว่างรันหากมีการใช้หน่วยความจำที่ถูกคืนแล้ว- การตรวจสอบครอบคลุมน้อยกว่าส่วนย่อยที่ปลอดภัยของ Rust
- มีค่าใช้จ่ายขณะรัน และโปรแกรมอาจ panic ได้
ReleaseFastจะตัดการตรวจสอบออกในโปรดักชัน แต่ยังคงไว้ในดีบักบิลด์และการทดสอบ- หากทดสอบทุกเส้นทางการทำงานจริงได้ ก็จะได้ความปลอดภัยระดับเดียวกับ
ReleaseSafeแต่โดยทั่วไปความครอบคลุมการทดสอบแบบนั้นไม่สมจริง
-
โปรเจ็กต์ Zig อื่น ๆ และ
unsafeของ Rust- TigerBeetle ใช้
ReleaseSafeและในการตรวจสอบ Jepsen อย่างละเอียด พบ safety bug 2 รายการ แต่ทั้งคู่ไม่เกี่ยวกับความปลอดภัยของหน่วยความจำ - Ghostty และคอมไพเลอร์ Zig ที่ใช้
ReleaseFastก็ไม่มี CVE ที่เกิดจากความไม่ปลอดภัยของหน่วยความจำในโค้ด Zig - โปรแกรม Rust ก็อาจมีช่องว่างด้านความปลอดภัยของหน่วยความจำผ่าน
unsafeภายใน dependency ได้เช่นกัน - Unsafe Rust มีความเสี่ยงแบบเดียวกับ Zig
ReleaseFastแต่ไม่มีฟีเจอร์ที่เทียบได้กับการตรวจสอบขณะรันของ Zig เพื่อจับปัญหาระหว่างพัฒนา - Miri และ Valgrind ช่วยได้ แต่มีโปรเจ็กต์ Rust ไม่มากที่ใช้งานสิ่งเหล่านี้
- วัฒนธรรมการใช้
unsafeอย่างจำกัดและการรีวิวที่เข้มงวดกว่าต่างหากที่สร้างชื่อเสียงด้านความปลอดภัยของหน่วยความจำอันแข็งแกร่งของ Rust ในงานจริง - แม้แต่ในโปรเจ็กต์ที่ใช้ Rust ก็มีกรณีที่เกิดช่องโหว่จาก
unsafe- Deno เคยมี CVE แบบอ่านนอกขอบเขตและ use-after-free
- Rocket เคยมี CVE แบบ use-after-free
- Actix มี CVE ด้านความไม่ปลอดภัยของหน่วยความจำหลายรายการในช่วงที่มีการใช้
unsafeสูงผิดปกติ
- Roc มองว่า use-after-free ไม่ใช่ความเสี่ยงใหญ่ เพราะการจัดสรรส่วนใหญ่เกิดขึ้นใน arena ที่มีอายุการใช้งานเรียบง่าย และเห็นว่าการตรวจสอบเพิ่มเติมของ Zig มีประโยชน์กับโค้ดที่ไม่ปลอดภัยโดยเนื้อแท้มากกว่า
- TigerBeetle ใช้
บั๊กหน่วยความจำเสียหายที่ตรวจพบหลังการเขียนใหม่
- ผลการจัดหมวดหมู่ issue tracker ของ Roc ด้วย Claude Opus 4.8 มีดังนี้
| คอมไพเลอร์ | เกิดหน่วยความจำเสียหาย | ไม่มีหน่วยความจำเสียหาย | รวม |
|---|---|---|---|
| Rust | 21 | 2,575 | 2,596 |
| Zig | 10 | 421 | 431 |
- กรณีหน่วยความจำเสียหาย 21 รายการในคอมไพเลอร์ Rust ไม่ได้เป็นความเสียหายในลอจิกภายในคอมไพเลอร์
- borrow checker ทำหน้าที่ตามที่ตั้งใจไว้
- เป็นบั๊ก miscompilation ที่สร้าง machine code ผิด ทำให้โปรแกรมที่ถูกคอมไพล์ไปทำให้หน่วยความจำเสียหาย
- ในบรรดา 10 กรณีหน่วยความจำเสียหายในคอมไพเลอร์ Zig ก็มี 8 กรณีที่เป็น miscompilation เช่นกัน
- อีก 2 กรณีที่เหลือเป็น use-after-free ในโค้ดรายงานข้อผิดพลาด
- ในข้อความผิดพลาดของ
roc checkและroc bundleชื่อไฟล์แสดงผลเพี้ยนเป็นอักขระแทนที่ U+FFFD - หากเป็น borrow checker ของ Rust ก็จะจับบั๊กทั้งสองได้
- ในข้อความผิดพลาดของ
- ผลลัพธ์ที่การเลือกเครื่องมือแต่ละแบบจะมีต่อผู้ใช้จริงมีดังนี้
| การเลือกเครื่องมือ | ผลกระทบจริงด้านความปลอดภัยของหน่วยความจำ |
|---|---|
Zig ReleaseFast |
บั๊ก 2 รายการที่ทำให้ชื่อไฟล์ไม่ถูกเรนเดอร์ในข้อความผิดพลาดบางส่วน |
Zig ReleaseSafe |
บั๊ก 2 รายการที่ทำให้ข้อผิดพลาดบางอย่าง panic และไม่สามารถเรนเดอร์ชื่อไฟล์ได้ |
| Rust borrow checker | ป้องกันบั๊กทั้งสองรายการ |
- เมื่อพิจารณา 18 เดือน, โค้ดหลายแสนบรรทัด, และรายงานบั๊กหลายร้อยรายการ ความต่างของทั้งสามทางเลือกไม่ได้มากพอจะมีนัยสำคัญในทางปฏิบัติต่อโปรเจ็กต์
- Bun ต้องจัดการทั้งค่าของ JavaScript แบบ tracing garbage collection และค่าที่จัดการด้วยมือ ทำให้ use-after-free, double-free และการลืมคืนหน่วยความจำมีสัดส่วนใหญ่
- คอมไพเลอร์ Roc ไม่ได้ทำงานร่วมกับ JavaScript หรือ tracing garbage collector อื่น ๆ จึงไม่เจอปัญหาการจัดการอายุการใช้งานแบบเดียวกัน
- Roc ต้องการเครื่องมือสำหรับค้นหา ข้อผิดพลาดด้านหน่วยความจำในโค้ดเอาต์พุตที่สร้างขึ้น มากกว่าข้อผิดพลาดหน่วยความจำภายในคอมไพเลอร์ และอย่างหลังนั้นอยู่นอกขอบเขตของ borrow checker
ผลลัพธ์จริงของเวลา build
zig build --watch -fincrementalปัจจุบัน rebuild การเปลี่ยนแปลงในโค้ด Zig ราว 450,000~460,000 บรรทัด ได้ในเวลาประมาณ 35ms- Zig 0.16.0 รุ่นเสถียรมีบั๊กที่ทำให้
-fincrementalใช้ไม่ได้กับโค้ดเบสของ Roc- มีการรวมแพตช์แก้ไขแล้ว แต่การใช้งานต้องย้ายไปใช้ prebuild ของ Zig 0.17.0 ที่มี breaking compatibility
- เนื่องจากต้อง vendor dependency ที่เกี่ยวข้องและอัปเกรดไป 0.17.0 ด้วย จึงตัดสินใจรอ stable release ถัดไป
- ผลการวัดบนเดสก์ท็อป Intel และ Ubuntu 26 มีดังนี้
| คอมไพเลอร์ Roc | ขนาดโค้ด | คอลด์บิลด์ | อินเครเมนทัลบิลด์ |
|---|---|---|---|
| ต้นฉบับเดิมบน Rust 1.85.0 | 354K | 32.4 วินาที | 10.0 วินาที |
| ต้นฉบับเดิมบน Rust 1.97.0 | 354K | 25.4 วินาที | 3.4 วินาที |
| Zig 0.16.0 ตอนที่ฟีเจอร์เทียบเท่ากัน | 320K | 39.6 วินาที | 8.6 วินาที |
| งานเขียนใหม่ปัจจุบันบน Zig 0.17.0 | 464K | 32.1 วินาที | 0.035 วินาที |
- ตอนที่ฟีเจอร์เทียบเท่ากัน ยังมีการ rebuild เอาต์พุตที่แทบไม่เปลี่ยนทุกครั้ง แต่ปัจจุบันจะสร้างใหม่เฉพาะเมื่อจำเป็น
- การเปลี่ยนแปลงนี้ทำให้โค้ดเพิ่มขึ้นราว 50% แต่คอลด์บิลด์กลับเร็วขึ้น
- ระหว่าง Rust 1.85.0 กับ 1.97.0 อินเครเมนทัลบิลด์ลดจาก 10 วินาทีเหลือ 3.4 วินาที หรือดีขึ้นประมาณสองในสามตลอด 18 เดือน
- 35ms ของ Zig คิดเป็นประมาณ 1/100 ของ 3.4 วินาที และเป็นผลที่ได้จากโค้ดเบสที่ใหญ่กว่าฝั่ง Rust ราว 50%
- ปัจจุบัน
-fincrementalใช้งานได้เฉพาะบน x86-64 และผู้ร่วมพัฒนาจำนวนมากใช้ Mac แบบ ARM จึงยังไม่สามารถใช้ประโยชน์จากข้อได้เปรียบด้านเวลา build ได้อย่างเต็มที่
โครงสร้างไร้พอยน์เตอร์และการดีซีเรียลไลซ์แบบไม่ต้องพาร์ส
- ดิสก์แคชตัวใหม่ใช้รูปแบบบนดิสก์ที่คงเลย์เอาต์หน่วยความจำซึ่งมีประสิทธิภาพต่อการรันไว้ตามเดิม คล้ายแนวทางที่ใช้ในคอมไพเลอร์ Zig และการพัฒนาเกม
- โครงสร้างข้อมูลทั้งหมดของคอมไพเลอร์แสดงผลด้วย อาร์เรย์ดัชนี 32 บิต แทนพอยน์เตอร์ และในหลายจุดใช้รูปแบบอาร์เรย์ของสตรักต์
- ลดการใช้หน่วยความจำและเพิ่มความเร็วในการรัน
- สามารถเขียนโครงสร้างข้อมูลลงดิสก์ได้โดยตรงโดยไม่ต้องแปลงเป็นรูปแบบซีเรียลไลซ์แยกต่างหาก
- ตอนดีซีเรียลไลซ์จะไม่พาร์สไบต์จากดิสก์
- อ่านไบต์เข้าไปในหน่วยความจำ
- จัดวางใหม่บางส่วนเพื่อให้โครงสร้างข้อมูลเดิมชี้ไปยังอาร์เรย์ใหม่
- จากนั้นก็ใช้งานได้ทันที
- ความเร็วถูกจำกัดในทางปฏิบัติด้วยความเร็ว I/O ของการอ่านไบต์จากดิสก์เข้าสู่หน่วยความจำ
- หากข้อมูลอยู่ในดิสก์แคชของระบบปฏิบัติการ ก็จะโหลดผลลัพธ์บิลด์ก่อนหน้าได้ด้วยความเร็วใกล้เคียง
memcpy
- หากข้อมูลอยู่ในดิสก์แคชของระบบปฏิบัติการ ก็จะโหลดผลลัพธ์บิลด์ก่อนหน้าได้ด้วยความเร็วใกล้เคียง
roc checkจะบันทึกผลของการพาร์ส การตรวจชนิด และอื่น ๆ ลงดิสก์ในการรันครั้งแรก- หากซอร์สอินพุตไม่เปลี่ยนแปลง ในการรันครั้งที่สองก็จะย้ายโครงสร้างข้อมูลจากดิสก์เข้าสู่หน่วยความจำได้โดยตรง
roc testยังแคชผลการทดสอบของ pure function แบบกำหนดแน่นอนได้ด้วย- แคชทำงานในระดับไฟล์ ดังนั้นเมื่อแก้ไฟล์หนึ่ง ก็จะประมวลผลใหม่เฉพาะไฟล์นั้นและไฟล์ที่พึ่งพามัน
- วิธีนี้เป็นไปได้เพราะทั้งคอมไพเลอร์ใช้ดัชนีแทนพอยน์เตอร์ และในโครงสร้างทั่วไปที่ยึดพอยน์เตอร์เป็นหลัก การดีซีเรียลไลซ์แบบไม่ต้องพาร์ส เป็นไปไม่ได้
-
ข้อจำกัดด้านความปลอดภัยของโครงสร้างแบบอิงดัชนี
- เช่นเดียวกับที่พอยน์เตอร์อาจชี้ไปยังแอดเดรสผิด ดัชนีก็อาจไปอ้างอิงอาร์เรย์ผิดและอ่านไบต์ตามอำเภอใจได้
- Rust borrow checker จัดการอายุการใช้งานของพอยน์เตอร์ แต่ไม่ตรวจว่าดัชนีใดสังกัดอาร์เรย์ใด
- หากรู้จำนวนอาร์เรย์ที่ต้องใช้ล่วงหน้า
compact_arenaของ Rust สามารถสร้าง type tag ด้วยแมโครเพื่อป้องกันการอ้างอิงอาร์เรย์ผิดได้ - แต่ถ้าจำนวนอาร์เรย์เปลี่ยนไปตามจำนวนโมดูลแบบ Roc ก็ใช้เทคนิคนี้ไม่ได้ ทำให้
compact_arenaเองก็ระบุSmallArena::newเป็นunsafe - การสร้าง arena ว่าง ๆ เองไม่ได้ก่อความเสี่ยง แต่ความเสี่ยงจริงอยู่ที่การทำ array indexing ซึ่งเกิดขึ้นบ่อยมาก
- Safe Rust มีประสิทธิภาพเมื่อ
unsafeมีขนาดเล็กและถูกแยกขอบเขตไว้ แต่ในกรณีแบบ Roc ที่unsafeกระจายกว้าง สมมติฐานนี้ใช้ไม่ได้
ระบบนิเวศของ Zig ที่เหมาะกับ Roc
- Bun ได้ประโยชน์จาก
Dropของ Rust ที่รันโค้ด cleanup เพียงครั้งเดียว เพราะต้องเชื่อมการทำงานระหว่าง JavaScript กับหน่วยความจำแบบจัดการเอง - ตรงกันข้าม Roc ต้องการใช้ arena แยกกัน ตามแต่ละโมดูลและแต่ละขั้นตอนการคอมไพล์ จึงใช้งานแพ็กเกจ Rust ที่ตั้งอยู่บนสมมติฐานของ global allocator และ
Dropแบบปริยายได้ไม่สะดวก - ระบบนิเวศของ Zig มักใช้งาน API ที่ส่ง allocator อย่างชัดเจน จึงเข้ากับแนวทางจัดการหน่วยความจำของ Roc ได้ดี
- ระบบนิเวศของ Rust เหมาะกับโครงสร้างที่ Bun ต้องการ ส่วนระบบนิเวศของ Zig เหมาะกับโครงสร้างที่ Roc ต้องการมากกว่า
-
การนำตัวซีเรียลไลเซอร์ LLVM bitcode กลับมาใช้ใหม่
- LLVM เป็นดีเพนเดนซีหลักของตัว optimizer ของ Roc และยังทำ optimization เพิ่มเติมหลังจาก Roc ทำ optimization ของตัวเองแล้ว
- LLVM มักทำลายความเข้ากันได้ของ API หลักอยู่บ่อยครั้ง ทำให้การอัปเกรดเวอร์ชันใช้เวลาและต้นทุนสูงพอสมควร
- รูปแบบ bitcode ที่ซีเรียลไลซ์ของ LLVM มีความเสถียรและรองรับย้อนหลังได้ จึงสามารถหลุดพ้นจากการเปลี่ยนแปลงของ C++ API ได้หากใช้ตัวซีเรียลไลเซอร์ของตัวเอง
- สำหรับสิ่งนี้ จำเป็นต้องมีตัวซีเรียลไลซ์ bitcode แบบเขียนเองที่แยกจากไลบรารี LLVM C++
- อิมพลีเมนเทชันที่เป็นที่รู้จักก่อนหน้านี้มีอยู่ในคอมไพเลอร์ Zig และคอมไพเลอร์ตัวใหม่ของ Roc ก็นำโค้ด Zig ส่วนนั้นกลับมาใช้
- แหล่งดีเพนเดนซีที่ Roc ได้จากระบบนิเวศ Zig มากที่สุดจึงไม่ใช่แพ็กเกจทั่วไป แต่คือ ตัวคอมไพเลอร์ Zig เอง
ฟีเจอร์ Rust ที่คิดถึงเมื่ออยู่ใน Zig
- ในการทำคอมไพเลอร์จำเป็นต้องควบคุมการจัดสรรอย่างชัดเจน แต่ในการทดสอบนั้น การจัดสรรและคืนหน่วยความจำอัตโนมัติ ของ Rust สะดวกกว่า
- test allocator ของ Zig สามารถตรวจจับ memory leak และยังจับ leak ของโค้ด Roc ที่คอมไพล์แล้วได้ด้วย
- แต่ในแต่ละเทสต์ต้องเขียน
initและdefer deinitให้ถูกต้องแม่นยำ และหากพลาดไปแม้แต่จุดเดียว เทสต์จะล้มเหลวเพราะถูกมองว่าเป็น leak
- แม้
comptimeของ Zig จะซ้อนทับกับ polymorphism แบบพารามิเตอร์และแบบเฉพาะกิจอยู่บ้าง แต่ก็ยังคิดถึง polymorphism ทั้งสองรูปแบบนั้น- trait
Allocatorของ Rust สามารถรับselfได้ - ส่วนอิมพลีเมนเทชันอย่าง
ArenaAllocatorของ Zig ต้องรับพอยน์เตอร์anyopaqueแล้วค่อยแคสต์กลับเป็นชนิดของตัวเอง
- trait
- ไม่มี ฟิลด์สตรักต์แบบ private จึงไม่สามารถให้คอมไพล์เออร์กันการเข้าถึงฟิลด์ที่ไม่ควรถูกแตะต้องโดยตรงได้
- เวลาตรวจ diff ของโค้ดจะเห็นเพียงการเข้าถึงฟิลด์ แต่ไม่เห็นเอกสารของสตรักต์ต้นทาง จึงต้องย้อนกลับไปตรวจแยกทุกครั้ง
- บางครั้งก็คิดถึงความสม่ำเสมอของ Rust ที่ฟังก์ชัน ตัวแปร และค่าคงที่ต่างใช้
snake_caseทั้งหมด unsafeและ borrow checker มีต้นทุนก็จริง แต่ก็ให้ความอุ่นใจตรงที่ปัญหาบางอย่างต้องกังวลเฉพาะภายในบล็อกunsafeเท่านั้น- ไม่ได้หมายความว่าควรเพิ่มความสามารถแบบเดียวกันนี้ให้ Zig
- ใน Zig มักพบ โค้ดตาย ช้ากว่าใน Rust
- ทั้งเครื่องมือในตัวของ Zig และ
tidy.zigของ TigerBeetle ก็ยังจับโค้ดตายบางส่วนไม่ได้ - โค้ดตายไม่ได้ถูกสร้างลงไบนารี จึงไม่กระทบผู้ใช้ แต่ไม่ดีต่อการดูแลโค้ดเบส
- ทั้งเครื่องมือในตัวของ Zig และ
- การอัปเกรดเวอร์ชันย่อยและการเปลี่ยน edition ใน Rust โดยมากทำได้ราบรื่น
- ปัจจุบัน Zig ยังไม่ได้มุ่งรักษาความเข้ากันได้ย้อนหลัง ซึ่งเป็นเงื่อนไขที่คาดไว้อยู่แล้วจึงไม่ใช่ปัญหาใหญ่ แต่ประสบการณ์อัปเกรดที่ง่ายของ Rust นั้นดีกว่า
สิ่งที่พอใจใน Zig
- Zig มีเสน่ห์ของ การออกแบบเชิงลบลดทอน ที่แลกการลดเครื่องมือคุ้นเคยแบบภาษาฟังก์ชันเพื่อให้ได้คุณสมบัติอื่นมา
- ไม่มีแมโคร และปัญหาหลายอย่างรวมถึง polymorphism แบบพารามิเตอร์สามารถแก้ได้ด้วย
comptimeหรือฟังก์ชันธรรมดา - ควบคุมการจัดวางข้อมูลได้ละเอียด
- ใช้ชนิดจำนวนเต็มที่ไม่ใช่กำลังของ 2 อย่าง
u7,u5ได้โดยไม่ต้องจัดการบิตแยกเอง - รองรับ packed struct โดยตรง
- สามารถ inline ฟังก์ชันที่ตำแหน่งเรียกใช้ แทนที่จะกำหนดที่ตำแหน่งประกาศ
- ใช้งานความสามารถที่ใน Rust ต้องพึ่ง crate แบบแมโครได้โดยไม่ต้องเพิ่มดีเพนเดนซี
- ใช้ชนิดจำนวนเต็มที่ไม่ใช่กำลังของ 2 อย่าง
- ชุดเครื่องมือบิลด์ของ Zig เหมาะกับการสร้าง ไบนารีแบบสแตนด์อโลน สำหรับ Alpine Linux และ WebAssembly
- รวมถึงรองรับบิลด์พิเศษที่คอมไพล์ builtins ซึ่งเทียบได้กับ standard library ของ Roc เป็น binary blob แบบทึบแสงแล้วฝังไว้ในไฟล์ปฏิบัติการสุดท้าย
- Uber ก็ใช้ toolchain ของ Zig ในโครงสร้างพื้นฐานด้านบิลด์ แม้จะไม่ได้ใช้ภาษา Zig โดยตรง
- การจัดการข้อผิดพลาดของ Zig ถือแม้แต่ heap allocation ที่ล้มเหลวว่าเป็นข้อผิดพลาดใน userspace ทั่วไป
- Roc เองก็ใช้กลยุทธ์คล้ายกัน โดยอาศัย anonymous sum type และ payload เพื่อให้ข้อผิดพลาดสะสมอย่างเป็นธรรมชาติ
- ชอบแนวทางของ Zig และ Roc มากกว่า
anyhow,thiserrorและการจัดการพื้นฐานที่อิงResultของ Rust - สำหรับไวยากรณ์การส่งต่อข้อผิดพลาดนั้น ชอบ
?แบบต่อท้ายของ Rust มากกว่าtryของ Zig จึงนำ ตัวดำเนินการ?แบบต่อท้าย มาใช้ใน Roc ด้วย - เมื่อมองทั้งโปรเจ็กต์ รวมถึง allocator API ที่ละเอียดและโค้ดนำกลับใช้ได้สำหรับคอมไพเลอร์ประสิทธิภาพสูง ก็พึงพอใจกับการเลือก Zig อย่างมาก
ขั้นตอนถัดไปของ Roc
- มีแผนจะออก 0.1.0 ของคอมไพเลอร์ตัวใหม่ในช่วงครึ่งหลังของปีนี้ ซึ่งจะเป็นรีลีสที่มีหมายเลขครั้งแรกของ Roc
- ก่อนถึงวันออกจริงก็สามารถลอง Nightly builds ได้ แต่ตอนนี้ยังมีบั๊กหลายจุด ฟีเจอร์ที่ยังไม่เสร็จ และเอกสารที่ยังไม่สมบูรณ์
- Roc Programming Language Foundation เป็นองค์กรไม่แสวงหากำไรแบบ 501(c)(3) ในสหรัฐฯ และเงินบริจาคจะถูกนำไปใช้หลัก ๆ เพื่อชดเชยผู้มีส่วนร่วม
- สามารถติดตามความคืบหน้าการพัฒนาในอนาคตและคำถามต่าง ๆ ได้ที่ Roc Zulip
2 ความคิดเห็น
ความคิดเห็นจาก Lobste.rs
ในเชิงตัวเลข 35ms ของ Zig เร็วกว่า 3.4 วินาทีของ Rust เกือบ 100 เท่า และโค้ดก็มากกว่าราว 50% แต่ในงานพัฒนาจริงความต่างนี้ดูเหมือนจะถูกพูดเกินจริง
ถ้าไม่ได้บิลด์คอมไพเลอร์ใหม่ทุกนาทีแต่บิลด์ทุก 10 นาที เวลาที่ประหยัดได้ก็มีแค่ราว 3 วินาที และ 3.4 วินาทีก็ถือว่าเร็วพออยู่แล้ว
เมื่อโค้ดเบสใหญ่ขึ้น Rust อาจช้าลงได้ แต่ เวลา cold build กลับแย่ลงเสียอีก และเมื่อรวมทั้งการเปลี่ยนโน้ตบุ๊กทุก 2 ปีและการปรับปรุงของตัวคอมไพเลอร์เอง ก็ยากจะฟันธงว่าเวลา build ของ Rust จะเพิ่มขึ้นต่อเนื่องในระยะยาว
โหมด
checkและ LSP ของ Rust เร็วกว่าการคอมไพล์ใหม่ทั้งก้อน แต่ยังไม่ถึงระดับเดียวกันถ้าใช้ชุด
--watch -fincrementalจะคอมไพล์ใหม่ทุกครั้งที่เซฟ ทำให้ได้ฟีดแบ็กที่เร็วและถี่มากฉันย้ายโปรเจกต์ส่วนตัวจาก Rust ไป Zig และก็ใช้ Rust ทำงานมาหลายปีแล้ว แต่ ความเร็วคอมไพล์ของ Zig เป็นการเปลี่ยนแปลงที่ทำให้หายใจโล่งขึ้นจริง ๆ
35ms นี่เร็วแบบน่าทึ่งมาก
แค่รีลิงก์อย่างเดียวก็น่าจะใช้เวลานานกว่านั้น เลยสงสัยว่าคอมไพเลอร์ทำอะไรอยู่จริง ๆ
ถ้าเปลี่ยน implementation ของฟังก์ชัน ก็แค่แทรกแอสเซมบลีใหม่เข้าไปในไบนารีเดิมได้เลย แต่ถ้ามีการ inline, เปลี่ยน function signature, มี dependency, หรือมีการย้ายตำแหน่งเพราะพื้นที่ไม่พอ ก็ต้องวิเคราะห์เพิ่ม
เมื่อแก้หรือเพิ่มฟังก์ชัน ก็จะหาพื้นที่ว่างที่ใหญ่พอในส่วน
.textของไฟล์ executable ผลลัพธ์ แล้วเขียน machine code ใหม่ลงไปถ้าพื้นที่ไม่พอ ก็อาจต้องขยาย
.textและย้ายข้อมูลอื่น แต่จะขยาย section แบบทวีคูณเพื่อเฉลี่ยต้นทุนหากจำเป็นก็จะเพิ่ม symbol table และ relocation entry ด้วย และยังเว้นพื้นที่ว่างไว้ล่วงหน้าสำหรับสิ่งเหล่านี้ด้วย ดังนั้นพอทำงานเสร็จก็แค่ปิดไฟล์ได้เลย
ถ้าประเมินจากผล Tracy ที่เห็นล่าสุด จากทั้งหมด 35ms ลิงเกอร์ใช้เวลาไปเพียง ประมาณ 1ms เท่านั้น
ตัวเลขที่บอกว่าในโค้ด Rust 300,000 บรรทัด มีการใช้
unsafeราว 1,200 ครั้ง ถือว่ามากกว่าที่คาดไว้ตัวอย่างเช่น Inko มีเพียง expression
unsafe { ... }162 จุด และฟังก์ชันunsafe87 ตัวที่ถูกเปิดให้โค้ดที่สร้างขึ้นผ่าน C ABI ใช้งานได้ โดยโค้ด Rust ทั้งหมดมีประมาณ 88,000 บรรทัดอย่างไรก็ตาม เนื่องจากวิธีทำงานของ Inkwell ทำให้ LLVM backend ทั้งก้อนแทบจะเป็นเขต
unsafeขนาดใหญ่ จึงสรุปจากการเทียบตรง ๆ ได้ยากฉันจะติดป้าย
unsafeให้เฉพาะฟังก์ชันที่กระทบต่อ memory safety จริง ๆ เช่น การจัดการ pointer โดยตรง แต่บางคนก็ใช้มันเป็นสัญญาณเตือนว่าต้องทำบางอย่างก่อนเพื่อหลีกเลี่ยง panicตัวเลขที่บอกว่าใน Rust standard library และ compiler มี
unsafeปรากฏ 40,000 ครั้ง นั้นไม่แม่นยำเพราะนับรวมแม้แต่คำที่อยู่ในเทสต์และคอมเมนต์ โดยส่วนใหญ่อยู่ใน standard library และในตัวคอมไพเลอร์เองมีไม่ถึง 2,000 ครั้งแม้จะนับรวมคอมเมนต์กับเทสต์แล้วก็ตาม
ระหว่างที่มีส่วนร่วมกับ rustc สัดส่วน PR ของฉันที่มี
unsafeอยู่ด้วยยังไม่ถึง 1% ด้วยซ้ำ จึงถือว่าโค้ดไม่ปลอดภัยภายในคอมไพเลอร์นั้นพบได้น้อยการที่ standard library ซึ่งทำหน้าที่สร้าง runtime พื้นฐานที่ Rust ทั้งระบบพึ่งพา มี
unsafeจำนวนมากถือเป็นเรื่องธรรมชาติ และโค้ดแบบนี้ไม่ว่าในภาษาไหนก็ไม่ปลอดภัยอยู่ดี ไม่ว่าจะประกาศชัดหรือแฝงไว้ก็ตามคอมไพเลอร์นั้นมีขนาดเล็กกว่า rustc ราว 10 เท่า แต่ใช้
unsafeพอ ๆ กัน จึงอาจยังไม่ถึงขั้นเรียกว่าระบาดไปทั่ว แต่ก็เจอบ่อยพอสมควร และน่าสงสัยว่าทำไมถึงต้องใช้มากกว่า rustc มากเราไม่ได้แยกวิเคราะห์
unsafeที่เกี่ยวกับ standard library ออกจากกันในทั้งสองโค้ดเบส แต่ถ้าคอมไพเลอร์ตัวใหม่ถูกเขียนด้วย Rust ก็คาดว่าน่าจะมีunsafeมากกว่า rustc เพราะโครงสร้างที่ใช้แคชและใช้อินเด็กซ์แทน pointerเราไม่ได้ตั้งใจจะลดทอนคุณค่าของงานของทีม Rust เพียงแต่อยากอธิบายการตัดสินใจและความคืบหน้าของเรา พร้อมกับให้ความเคารพต่อผลงานของโปรเจกต์อื่น
เป็นบทความที่ประเมินข้อดีข้อเสียของ Rust และ Zig อย่างสมดุลโดยไม่ปิดบัง และข้อสรุปที่ซื่อตรงเป็นพิเศษคือ ต่อให้หลังจากเขียนโค้ดไปหลายแสนบรรทัดตลอด 18 เดือนและจัดการบั๊กไปหลายร้อยรายการแล้ว แต่ถ้าเลือกภาษาอื่นในตัวเลือก ผลลัพธ์ของโปรเจ็กต์ก็คงไม่ต่างไปมากนัก
อย่างไรก็ตาม คำอธิบายว่าการทำงานกับ หน่วยความจำแบบไม่ปลอดภัย เป็นส่วนใหญ่ของงานในคอมไพเลอร์ที่สร้างโค้ดเครื่องอย่าง Roc หรือ rustc นั้นเข้าใจได้ยาก
มีคอมไพเลอร์จำนวนมากที่เขียนด้วย OCaml หรือ Haskell อยู่แล้ว และการสร้างโค้ดเครื่องเองก็เป็นเพียงการประกอบไบต์ลงในเวกเตอร์แล้วเขียนเป็นไฟล์ จึงไม่มีเหตุผลว่าทำไมต้องไม่ปลอดภัย
ถ้าเป็นการแปลความหรือ JIT compilation ก็พอเข้าใจได้ แต่ก็ยังสงสัยว่าเหตุใดจึงจำเป็นแม้แต่กับการคอมไพล์ทั่วไป
ความเสี่ยงเกิดขึ้นตอนนำโค้ดเครื่องที่สร้างแล้วไปรัน และในทางปฏิบัติคอมไพเลอร์จำนวนมากก็มักสร้างและรันโค้ดเครื่องไปพร้อมกัน จึงใช้คำว่าเป็น ส่วนใหญ่ของงาน
นอกจากการแปลความและ JIT แล้ว ยังรวมถึงงานอย่างการประเมินโค้ดของผู้ใช้ในช่วงคอมไพล์ เช่น
const fnของ Rust หรือ expression ที่ยกขึ้นระดับบนสุดได้ใน Roc ตลอดจนการรันเทสต์แล้วตรวจผลลัพธ์เพื่อกำหนดว่าจะนำอะไรไปแสดงให้ผู้ใช้เห็นไม่ว่าหน่วยความจำที่เสียหายจะอยู่ในโปรเซสของคอมไพเลอร์หรือในโปรแกรมที่ถูกสร้างขึ้น สาเหตุที่โปรเซสเซอร์ทำงานผิดก็ยังเป็นคำสั่งที่คอมไพเลอร์สร้าง และจุดที่ต้องแก้ก็ยังเป็นโค้ดของคอมไพเลอร์ตามเกณฑ์นี้
คอมโพเนนต์อย่างโค้ดลิงก์ภายนอกและ garbage collector ต้องใช้
unsafeมากใน Rust เช่นกัน จนหักล้างข้อดีของ Rust แบบปลอดภัยไปไม่น้อยแม้จะเขียนเฉพาะคอมไพเลอร์ด้วยภาษาที่ปลอดภัยได้ แต่ปัญหาคือเรื่องประสิทธิภาพ และทั้ง Zig กับคอมไพเลอร์ของ Roc ก็ใช้ โครงสร้างแบบ array (SoA) รวมถึงใช้อินเด็กซ์ของอาร์เรย์อย่างกว้างขวางแทนพอยน์เตอร์
หากนำไปทำใน Rust ก็จะต้องเลี่ยง borrow checker ทำให้เสียข้อได้เปรียบด้านความปลอดภัยที่เกี่ยวข้องไป
ความสัมพันธ์ของ lifetime ในคอมไพเลอร์กลับเรียบง่ายกว่าที่คิด คือในแต่ละเฟสจะจัดสรร สร้าง และแก้ไขข้อมูลใน arena แล้วส่งต่อเป็นแบบอ่านอย่างเดียวไปยังเฟสถัดไป ก่อนจะทิ้ง arena ทั้งก้อนได้
ส่วนที่ซับซ้อนกว่าคือ incremental compilation และการลิงก์ที่อ่านสถานะจากดิสก์และแก้ไขไบนารีแบบ in-place ซึ่งอาจทำให้เกิดสถานะเสียหาย บั๊ก การคอมไพล์ผิด หรือปัญหาหน่วยความจำได้ แต่เป็นคนละเรื่องกับ memory safety ของโปรเซสคอมไพเลอร์เอง
ความปลอดภัยและความถูกต้อง เป็นแนวคิดที่กว้างกว่าความปลอดภัยของหน่วยความจำภายในโปรแกรมมาก โดยเฉพาะเมื่อพยายามทำงานที่แสดงออกใน Rust ได้ยากให้ปลอดภัยและถูกต้อง
ในฐานะผู้สร้าง
compact_arenaก็ดีใจที่มีการพูดถึง แต่การอธิบายวิธีใช้unsafeในไลบรารีนี้คลาดเคลื่อนเป้าหมายของ
compact_arenaคือทำให้ การทำอินเด็กซ์ปลอดภัย โดยรับประกันว่าไม่สามารถใช้newผิดวิธีได้ใน Rust แบบปลอดภัยมาโคร
mk_arenaสามารถเรียกใช้จากโค้ด Rust แบบปลอดภัยได้แม้อยู่ในลูปที่ไม่รู้จำนวน arena ที่ต้องใช้ล่วงหน้าnewอย่างปลอดภัย แต่ในส่วนความปลอดภัยของ https://docs.rs/compact_arena/0.5.0/… ระบุว่าแท็กที่ส่งให้คอนสตรักเตอร์เป็นพื้นฐานของเทคนิคการทำอินเด็กซ์ และถ้านำค่านี้ไปใช้กับ arena อื่น อินเด็กซ์ของสอง arena จะปะปนกันจนเกิดการเข้าถึงนอกขอบเขตและ พฤติกรรมที่ไม่กำหนด ได้ประเด็นของผมไม่ใช่ที่ตัวการเรียก
new()เอง แต่คือความเสี่ยงจะเกิดขึ้นตอนนำค่าที่สร้างด้วยnew()ไปทำอินเด็กซ์ และจุดที่ควรถูกตรวจสอบกับทำเครื่องหมายunsafeก็ควรเป็นจุดเรียกทำอินเด็กซ์จริง ๆแต่ก็เป็นไปได้ว่าผมเข้าใจการออกแบบผิด
เท่าที่รู้ Python ต้องรีสตาร์ตโปรแกรมเพื่อให้โค้ดใหม่มีผล และการทำ hot reloading ต้องอาศัยส่วนขยายที่ไม่เป็นมาตรฐานรวมถึงการเขียนโค้ดอย่างระมัดระวัง
hot reloading เป็นเรื่องที่พบได้ทั่วไปกว่าในสภาพแวดล้อมการพัฒนาแบบ image-based อย่าง Lisp หรือ Smalltalk หรือใน Erlang
importlib.reload()ก็ทำ hot reloading ใน Python ได้ แต่ไม่แน่ใจว่าใช้กันแพร่หลายแค่ไหนสงสัยว่าถ้าไม่ใช้
cargo buildจะช่วยลด เวลา build ได้หรือไม่ยังไม่แน่ใจว่าการเซ็ตอัป Bazel จะงานมากหรือน้อยกว่าการย้ายไป Zig แต่แน่นอนว่าเวลา build ไม่ใช่เหตุผลเดียวของการเปลี่ยนภาษา
ความคิดเห็นจาก Hacker News
โดยรวมบทความก็โอเค แต่ยากจะเห็นด้วยกับคำกล่าวที่ว่า ในคอมไพเลอร์ที่สร้างโค้ดเครื่องอย่าง Roc หรือ rustc นั้น งานที่ไม่ปลอดภัยต่อหน่วยความจำ เป็นส่วนใหญ่ของงาน
การแพตช์ไบนารีระหว่างรันหรือการโหลดโค้ดใหม่อาจต้องใช้โค้ดที่ไม่ปลอดภัย แต่ในกระบวนการสร้างไฟล์ปฏิบัติการตามปกติ การสร้างโค้ดเครื่องเองไม่ได้มีเหตุผลที่จะต้องไม่ปลอดภัย
ตรงกันข้าม มีแนวโน้มจะพบโค้ดที่ไม่ปลอดภัยใน รันไทม์ ของภาษามากกว่า
อย่างไรก็ตาม ในทางปฏิบัติคอมไพเลอร์จำนวนมากสร้างโค้ดเครื่องแล้วรันมันโดยตรงด้วย จึงใช้คำว่า “เป็นส่วนใหญ่ของงาน” แต่ไม่ได้แปลว่าคอมไพเลอร์จำเป็นต้องทำทั้งสองอย่างเสมอไป
ที่นึกถึงมีทั้งการแพตช์ไบนารี การโหลดโค้ดใหม่ รันไทม์ ตลอดจน การประเมินโค้ดของผู้ใช้ตอนคอมไพล์ เช่น
const fnของ Rust หรือ expression ที่ Roc สามารถย้ายขึ้นไปไว้ระดับบนสุดได้ รวมถึงการรันเทสต์แล้วตรวจผลลัพธ์เพื่อกำหนดสิ่งที่จะแสดงคล้ายกับตรรกะที่ว่าต่อให้คาดเข็มขัดนิรภัยก็ยังหัวกระแทกได้ ดังนั้นเข็มขัดนิรภัยมีแต่ความน่ารำคาญและไม่ควรใช้
ในโค้ดที่เน้นประสิทธิภาพสุดขั้ว โครงสร้างข้อมูล อัลกอริทึม และกลยุทธ์การจัดสรรหน่วยความจำย่อมต่างออกไป และ TigerBeetle ก็เป็นที่รู้จักจากการจัดสรรหน่วยความจำทั้งหมดรวดเดียวตั้งแต่เริ่มต้น
คอมไพเลอร์ Roc ก็กำลังพยายามเลือกทางประนีประนอมคล้าย Zig จึงเป็นธรรมดาที่จะพบแพตเทิร์นร่วมกันหลายอย่าง
ไม่เห็นหลักฐานรองรับคำกล่าวที่ว่า
ReleaseSafeจับ use-after-free ได้ด้วยการตรวจสอบตอนรันไทม์ลองค้นเอกสารของ Zig เกี่ยวกับการตรวจสอบความปลอดภัยของหน่วยความจำตอนรันไทม์แล้ว แต่ไม่พบอะไรเกี่ยวข้องเลย ไม่ว่าจะค้น
use-after-free,UaF, หรือsafety-checkedและถึงจะใช้DebugAllocatorในรีลีสบิลด์ ก็ไม่ได้ตรวจจับสิ่งนี้ได้อย่างน่าเชื่อถือมีการสรุปประเด็นนี้ไว้ใน https://landaire.net/memory-safety-by-default-is-non-negotia...
นี่เป็นความเห็นที่ได้จากการเขียนโค้ดเองก่อนยุค AI ไม่ใช่จากการให้ AI ช่วย และแม้การจับคู่ Zig กับ LLM อาจทำให้เกิดอะไรที่ต่างออกไป แต่ยิ่งอ่านก็ยิ่งเห็นคำกล่าวที่เขียนไว้แปลก ๆ จนเชื่อถือได้ยาก
มันอ่านแล้วเหมือนบทความที่เขียนขึ้นเพื่อทำให้ข้อถกเถียงเดิมดูสมเหตุสมผลมากกว่าจะเป็นการตัดสินทางเทคนิคอย่างตรงไปตรงมา แต่เพราะผมชอบบทความและภาษาที่แปลกดี และก็ไม่ชอบกระแส AI ที่เกินจริงอยู่แล้ว เลยพยายามมองในแง่ดีไว้ก่อน
ReleaseSafeแค่เพิ่ม การตรวจขอบเขต และทำให้เกิด panic เมื่อไปถึงโค้ดที่ไม่ควรเข้าถึงZig ดูเหมือนจะไม่ได้ให้ temporal memory safety
น่าสนใจที่แม้ OCaml จะสุกงอม ยืดหยุ่น และมีพลังในการแสดงออกพอสำหรับการพิสูจน์ต้นแบบ แต่ก็ไม่ได้ถูกเลือกเป็นภาษาสำหรับการใช้งานขั้นสุดท้าย
ก็ยังสงสัยว่า incremental build ของ Zig จะเร็วกว่า dune แบบมีนัยสำคัญจริงหรือไม่ และแม้ cross-compilation จะเป็นข้อดี แต่ในส่วน “ทำไมต้อง Zig” กลับไม่ได้พูดถึง
อยากรู้เหมือนกันว่าการควบคุมหน่วยความจำอย่างละเอียดสำคัญกับคอมไพเลอร์ขนาดนั้นจริงไหม และเหมือนกรณี Rust กับ WASM ที่เริ่มจาก OCaml ผู้ดูแลโครงการมักตัดสินใจเปลี่ยนภาษาในหมุดหมายแบบไหน
incremental build ของ Zig เป็นฟีเจอร์ชี้ขาดอย่างไม่ต้องสงสัย และก็เข้าใจได้หากจะยอมเปลี่ยนภาษาเพื่อให้ได้สิ่งนี้ในระยะสั้น
แต่ในระยะกลางก็หวังว่า Rust จะมีฟีเจอร์คล้ายกันเพิ่มเข้ามาในอนาคตอันใกล้
ผมอยากได้ความเร็ว แต่ก็ไม่อยากรีบแล้วมาสร้างปัญหาใหญ่ให้ตัวเอง และกำลังทำภาษาเองที่รวมความปลอดภัยแบบ Rust ฟีเจอร์แบบ Zig และรันไทม์แบบ Go ที่ไม่มี garbage collection
แนวทางนี้ดูมีโอกาสเป็นจริงมากกว่า และน่าจะทำได้จากฝั่ง user space ด้วย
ถ้าหากมีการเปรียบเทียบเชิง วิทยาศาสตร์ อย่างแท้จริงในกระบวนการเลือกภาษาที่ใช้พัฒนาคอมไพเลอร์ ก็น่าจะโน้มน้าวได้มากกว่านี้
ดูเหมือนว่าจะเริ่มจากสมมติฐานที่ยังไม่ได้รับการพิสูจน์ว่าคอมไพเลอร์ประสิทธิภาพสูงจำเป็นต้องใช้ภาษาระบบระดับล่าง(https://www.roc-lang.org/faq#self-hosted-compiler) แล้วจึงสรุปว่า Zig เป็นตัวเลือกเดียวที่เหลือนอกเหนือจาก Rust
ประสิทธิภาพของคอมไพเลอร์ถูกกำหนดโดยอัลกอริทึมเป็นหลัก และแม้แต่ภาษา managed ที่เร็ว หากใช้อัลกอริทึมเดียวกัน โดยมากเวลาในการรันก็มักต่างกันไม่เกิน 2 เท่า ขณะที่ช่องว่างด้านประสิทธิภาพจากความแตกต่างของอัลกอริทึมนั้นแทบไม่มีเพดาน
ตัว Zig เองก็เป็นตัวอย่างโต้แย้งต่อทฤษฎีที่ว่าการเขียนคอมไพเลอร์ด้วยภาษาระดับล่างจะทำให้เร็วขึ้น และความเร็วของ Roc ที่ประมาณ 15,000 บรรทัดต่อวินาที ก็ไม่ได้ถือว่าเร็ว มีข้อมูลว่าตั้งแต่ปี 1998 คอมไพเลอร์ ML ก็ประมวลผลได้ 3,000 บรรทัดต่อวินาทีแล้ว(https://flint.cs.yale.edu/cs421/case-for-ml.html)
ตอนนี้การหยุดงานคอมไพเลอร์ปัจจุบันไว้ก่อน แล้วไปสร้าง self-hosting compiler สำหรับชุดย่อยของ Roc ที่เล็กที่สุดซึ่งทำได้ภายในไม่ถึง 10,000 บรรทัด อาจเป็นประโยชน์ต่ออนาคตมากกว่า
แบบนั้นจะได้ทดลองหลายแนวทางบนโค้ดระดับ 10,000 บรรทัด แทนที่จะเป็น implementation ขนาด 300,000 บรรทัด และตรวจสอบได้ว่าภาษาระดับล่างจำเป็นต่อเป้าหมายด้านประสิทธิภาพจริงหรือไม่
ในกระบวนการ self-hosting ก็จะเห็นชัดขึ้นว่าฟีเจอร์ไหนของ Roc สำคัญจริง อีกทั้งยังได้เขียนโค้ด Roc มากขึ้น และถ้าปรับปรุงฟีเจอร์ทั่วไปที่คอมไพเลอร์ต้องใช้ แอปพลิเคชันชั้นล่างก็จะดีขึ้นไปพร้อมกัน
เพียงแค่รู้ว่าในยุค 1990 เคยมีการคอมไพล์ ML ได้เร็ว ก็ยังตัดสินความเร็วคอมไพล์ของ Roc ในปัจจุบันได้ยาก เพราะการออกแบบภาษาส่งข้อจำกัดอย่างมากต่ออัลกอริทึมที่ใช้ และฮาร์ดแวร์สมัยใหม่ก็ซับซ้อนกว่ามาก
Roc ดูเหมือนจะมี overloading อยู่บ้าง และยังมีอัลกอริทึมที่ซับซ้อนเพื่อหลีกเลี่ยงการจัดสรร closure ลงบน heap ซึ่งข้อกำหนดเหล่านี้อาจก่อให้เกิดความซับซ้อนของอัลกอริทึมที่เลี่ยงไม่ได้
เมื่อไปถึงขีดจำกัดของการปรับอัลกอริทึมแล้ว สิ่งที่เหลือก็คือ การลด constant factor และโดยเฉพาะในภาษาระดับสูงที่มีการจัดการหน่วยความจำ ก็มีขีดล่างที่ชัดเจนว่าลดส่วนนี้ลงได้มากแค่ไหน
ผมเคยเห็นกรณีในโค้ดจริงที่การควบคุม layout ของหน่วยความจำโดยตรงทำให้ประสิทธิภาพดีขึ้นมากกว่า 10 เท่า และในวงการเกม งานลักษณะนี้ก็อาจกินสัดส่วนใหญ่ของอาชีพเลยด้วยซ้ำ สภาพแวดล้อมที่มีอัลกอริทึมอันชาญฉลาดเพียงตัวเดียวแล้วปัญหาด้านประสิทธิภาพทั้งหมดหายไปนั้นห่างไกลจากความเป็นจริง
บิลด์ของ Rust เป็นหนึ่งในสาเหตุหลักที่ทำให้คอมพิวเตอร์ทุกเครื่องสิ้นเปลืองพื้นที่เก็บข้อมูลอย่างมาก และถ้าบิลด์หลายไลบรารี พื้นที่ระดับหลายสิบ GB ก็สะสมขึ้นอย่างรวดเร็ว
แม้จะสามารถตั้งค่า โฟลเดอร์บิลด์ส่วนกลาง ให้ใช้ dependency ร่วมกันข้ามโปรเจกต์ได้ แต่ไม่ว่าแนวทางไหนก็ควรมีให้เป็นพฤติกรรมเริ่มต้น
อย่างไรก็ดี จำเป็นต้องมีการเก็บกวาดแคช และตอนนี้งานจัดวางผลลัพธ์บิลด์ขั้นกลางแบบใหม่ที่ช่วยให้เรื่องนี้ง่ายขึ้นก็กำลังอยู่ในช่วงท้ายแล้ว
node_modulesใน ecosystem ของ JavaScript ถูกวิจารณ์อยู่เสมอ ก็เป็นความย้อนแย้งที่น่าสนใจแม้แต่ในโปรเจกต์ Tauri ที่โค้ดฝั่ง backend เล็กกว่าฝั่ง frontend มาก ผลลัพธ์บิลด์ของ Rust ก็มีขนาด 9GB ขณะที่
node_modulesมีเพียง 550MBผมเข้าใจ Roc ว่าเป็นภาษาสคริปต์ที่ฝังเข้าไปใน C ABI และสงสัยว่า กรณีการใช้งาน จริงคืออะไร
อยากรู้ว่ากำลังพยายามแข่งขันกับ WASM ในสภาพแวดล้อมปลั๊กอินที่มีแพลตฟอร์ม Roc ขนาดใหญ่หรือไม่ และถ้าเป็นเช่นนั้น เหตุใดนักพัฒนาแอปจึงต้องเปิดชั้น Roc ออกมา แทนที่จะใช้ WASM ที่เปิดให้ผู้พัฒนาปลั๊กอินเลือกภาษาใดก็ได้
ถ้าเป็นภาษาระดับแอปพลิเคชันที่ใช้แพลตฟอร์ม Roc ขนาดเล็ก ก็สงสัยเหมือนกันว่าตั้งใจจะแข่งกับ Gleam ในโค้ด HTTP ฝั่งเซิร์ฟเวอร์ และแข่งกับ Elm ในโค้ดฝั่งไคลเอนต์หรือไม่
เวลาในการคอมไพล์ เป็นปัจจัยที่ถูกประเมินค่าต่ำอย่างมาก การต้องรอบิลด์ C++ ครั้งละ 10 นาทีคือความไม่พอใจที่สุด และมันตัดสมาธิการพัฒนาไปอย่างสิ้นเชิง
แค่เปลี่ยนจากไฟล์
.rsไปเป็นไฟล์.tsก็รู้สึกได้ถึงความต่างราวกับเปลี่ยนคอมพิวเตอร์ทั้งเครื่องแม้จะไม่ได้ใช้ Zig โดยตรง แต่ก็ยังคาดหวังกับความเป็นไปได้บางอย่างมาก
โดยเฉพาะ เกมรูปแบบใหม่ ที่เขียนด้วย Zig ซึ่งใช้งานสะดวกกว่า C, ซอฟต์แวร์กระจายศูนย์ ที่ TigerBeetle แสดงให้เห็นศักยภาพแล้ว และสายงาน robotics ที่ผมสนใจเป็นการส่วนตัว