2 คะแนน โดย GN⁺ 4 시간 전 | 2 ความคิดเห็น | แชร์ทาง WhatsApp
  • เพื่อ แก้ไขข้อบกพร่องเชิงโครงสร้าง ของอิมพลีเมนเทชันเดิม จึงเขียนใหม่จาก 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 บนหน้าเว็บไซต์หลัก

เหตุผลที่เขียนใหม่ตั้งแต่ต้น

  • 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 จะเร็วขึ้นมาก
  • การควบคุมหน่วยความจำและการจัดวางข้อมูล

    • ในแต่ละขั้นของคอมไพล์ มีการใช้ 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 มีโมเดลที่แยก unsafe block ซึ่งพบไม่บ่อยออกมาต่างหาก แล้วตรวจด้วย Miri หรือ Valgrind แต่สำหรับ Roc นั้น unsafe ไม่ใช่สิ่งที่พบได้ยาก
    • Zig มีฟีเจอร์สำหรับทำให้โค้ดที่ไม่ปลอดภัยต่อหน่วยความจำทำงานได้อย่างถูกต้องมากกว่า และทีมก็ต้องการการสนับสนุนมากที่สุดในพื้นที่นี้โดยตรง

ความปลอดภัยของหน่วยความจำที่ได้มาโดยไม่ต้องมี 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 มีประโยชน์กับโค้ดที่ไม่ปลอดภัยโดยเนื้อแท้มากกว่า

บั๊กหน่วยความจำเสียหายที่ตรวจพบหลังการเขียนใหม่

  • ผลการจัดหมวดหมู่ 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 แล้วค่อยแคสต์กลับเป็นชนิดของตัวเอง
  • ไม่มี ฟิลด์สตรักต์แบบ private จึงไม่สามารถให้คอมไพล์เออร์กันการเข้าถึงฟิลด์ที่ไม่ควรถูกแตะต้องโดยตรงได้
    • เวลาตรวจ diff ของโค้ดจะเห็นเพียงการเข้าถึงฟิลด์ แต่ไม่เห็นเอกสารของสตรักต์ต้นทาง จึงต้องย้อนกลับไปตรวจแยกทุกครั้ง
  • บางครั้งก็คิดถึงความสม่ำเสมอของ Rust ที่ฟังก์ชัน ตัวแปร และค่าคงที่ต่างใช้ snake_case ทั้งหมด
  • unsafe และ borrow checker มีต้นทุนก็จริง แต่ก็ให้ความอุ่นใจตรงที่ปัญหาบางอย่างต้องกังวลเฉพาะภายในบล็อก unsafe เท่านั้น
    • ไม่ได้หมายความว่าควรเพิ่มความสามารถแบบเดียวกันนี้ให้ Zig
  • ใน Zig มักพบ โค้ดตาย ช้ากว่าใน Rust
    • ทั้งเครื่องมือในตัวของ Zig และ tidy.zig ของ TigerBeetle ก็ยังจับโค้ดตายบางส่วนไม่ได้
    • โค้ดตายไม่ได้ถูกสร้างลงไบนารี จึงไม่กระทบผู้ใช้ แต่ไม่ดีต่อการดูแลโค้ดเบส
  • การอัปเกรดเวอร์ชันย่อยและการเปลี่ยน edition ใน Rust โดยมากทำได้ราบรื่น
  • ปัจจุบัน Zig ยังไม่ได้มุ่งรักษาความเข้ากันได้ย้อนหลัง ซึ่งเป็นเงื่อนไขที่คาดไว้อยู่แล้วจึงไม่ใช่ปัญหาใหญ่ แต่ประสบการณ์อัปเกรดที่ง่ายของ Rust นั้นดีกว่า

สิ่งที่พอใจใน Zig

  • Zig มีเสน่ห์ของ การออกแบบเชิงลบลดทอน ที่แลกการลดเครื่องมือคุ้นเคยแบบภาษาฟังก์ชันเพื่อให้ได้คุณสมบัติอื่นมา
  • ไม่มีแมโคร และปัญหาหลายอย่างรวมถึง polymorphism แบบพารามิเตอร์สามารถแก้ได้ด้วย comptime หรือฟังก์ชันธรรมดา
  • ควบคุมการจัดวางข้อมูลได้ละเอียด
    • ใช้ชนิดจำนวนเต็มที่ไม่ใช่กำลังของ 2 อย่าง u7, u5 ได้โดยไม่ต้องจัดการบิตแยกเอง
    • รองรับ packed struct โดยตรง
    • สามารถ inline ฟังก์ชันที่ตำแหน่งเรียกใช้ แทนที่จะกำหนดที่ตำแหน่งประกาศ
    • ใช้งานความสามารถที่ใน Rust ต้องพึ่ง crate แบบแมโครได้โดยไม่ต้องเพิ่มดีเพนเดนซี
  • ชุดเครื่องมือบิลด์ของ 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 ความคิดเห็น

 
GN⁺ 3 시간 전
ความคิดเห็นจาก Lobste.rs
  • ในเชิงตัวเลข 35ms ของ Zig เร็วกว่า 3.4 วินาทีของ Rust เกือบ 100 เท่า และโค้ดก็มากกว่าราว 50% แต่ในงานพัฒนาจริงความต่างนี้ดูเหมือนจะถูกพูดเกินจริง
    ถ้าไม่ได้บิลด์คอมไพเลอร์ใหม่ทุกนาทีแต่บิลด์ทุก 10 นาที เวลาที่ประหยัดได้ก็มีแค่ราว 3 วินาที และ 3.4 วินาทีก็ถือว่าเร็วพออยู่แล้ว
    เมื่อโค้ดเบสใหญ่ขึ้น Rust อาจช้าลงได้ แต่ เวลา cold build กลับแย่ลงเสียอีก และเมื่อรวมทั้งการเปลี่ยนโน้ตบุ๊กทุก 2 ปีและการปรับปรุงของตัวคอมไพเลอร์เอง ก็ยากจะฟันธงว่าเวลา build ของ Rust จะเพิ่มขึ้นต่อเนื่องในระยะยาว

    • การที่ ข้อผิดพลาดตอนคอมไพล์และผลทดสอบที่ใช้ไม่ได้แล้ว โผล่มาทันทีหลังเซฟ ก่อนจะกดคีย์ถัดไปหรือย้ายตำแหน่งแก้ไขด้วยซ้ำ เป็นข้อได้เปรียบมหาศาล
      โหมด check และ LSP ของ Rust เร็วกว่าการคอมไพล์ใหม่ทั้งก้อน แต่ยังไม่ถึงระดับเดียวกัน
    • ถ้าสมมติว่าคอมไพล์ใหม่แค่ทุก 10 นาทีครั้งเดียว ก็จะประเมินความต่างต่ำเกินไป
      ถ้าใช้ชุด --watch -fincremental จะคอมไพล์ใหม่ทุกครั้งที่เซฟ ทำให้ได้ฟีดแบ็กที่เร็วและถี่มาก
      ฉันย้ายโปรเจกต์ส่วนตัวจาก Rust ไป Zig และก็ใช้ Rust ทำงานมาหลายปีแล้ว แต่ ความเร็วคอมไพล์ของ Zig เป็นการเปลี่ยนแปลงที่ทำให้หายใจโล่งขึ้นจริง ๆ
  • 35ms นี่เร็วแบบน่าทึ่งมาก
    แค่รีลิงก์อย่างเดียวก็น่าจะใช้เวลานานกว่านั้น เลยสงสัยว่าคอมไพเลอร์ทำอะไรอยู่จริง ๆ

    • Zig มีลิงเกอร์ของตัวเอง
    • หัวใจสำคัญคือ incremental linking และการแก้ไขไบนารีเดิมแบบ in-place ซึ่งจำได้ว่าในคอมไพเลอร์ Roc เรียกว่า surgical linking
      ถ้าเปลี่ยน implementation ของฟังก์ชัน ก็แค่แทรกแอสเซมบลีใหม่เข้าไปในไบนารีเดิมได้เลย แต่ถ้ามีการ inline, เปลี่ยน function signature, มี dependency, หรือมีการย้ายตำแหน่งเพราะพื้นที่ไม่พอ ก็ต้องวิเคราะห์เพิ่ม
    • ทีม Zig รับผิดชอบงานด้าน incremental compile ไปมาก และ ELF linker ตัวใหม่ก็ยังไม่เสร็จสมบูรณ์ แต่ถูกออกแบบมาโดยมีเป้าหมายเพื่อ incremental linking ระดับฟังก์ชันเดี่ยวและตัวแปร global
      เมื่อแก้หรือเพิ่มฟังก์ชัน ก็จะหาพื้นที่ว่างที่ใหญ่พอในส่วน .text ของไฟล์ executable ผลลัพธ์ แล้วเขียน machine code ใหม่ลงไป
      ถ้าพื้นที่ไม่พอ ก็อาจต้องขยาย .text และย้ายข้อมูลอื่น แต่จะขยาย section แบบทวีคูณเพื่อเฉลี่ยต้นทุน
      หากจำเป็นก็จะเพิ่ม symbol table และ relocation entry ด้วย และยังเว้นพื้นที่ว่างไว้ล่วงหน้าสำหรับสิ่งเหล่านี้ด้วย ดังนั้นพอทำงานเสร็จก็แค่ปิดไฟล์ได้เลย
      ถ้าประเมินจากผล Tracy ที่เห็นล่าสุด จากทั้งหมด 35ms ลิงเกอร์ใช้เวลาไปเพียง ประมาณ 1ms เท่านั้น
  • ตัวเลขที่บอกว่าในโค้ด Rust 300,000 บรรทัด มีการใช้ unsafe ราว 1,200 ครั้ง ถือว่ามากกว่าที่คาดไว้
    ตัวอย่างเช่น Inko มีเพียง expression unsafe { ... } 162 จุด และฟังก์ชัน unsafe 87 ตัวที่ถูกเปิดให้โค้ดที่สร้างขึ้นผ่าน 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 มาก

    • โค้ดเบสของเราก็เป็นการผสมกันของคอมไพเลอร์กับ standard library และ standard library บางส่วนก็ถูกทำเป็น compiler intrinsic ดังนั้นสำหรับตัวชี้วัดนี้จึงถือว่าเทียบสิ่งที่ใกล้เคียงกันอยู่
      เราไม่ได้แยกวิเคราะห์ unsafe ที่เกี่ยวกับ standard library ออกจากกันในทั้งสองโค้ดเบส แต่ถ้าคอมไพเลอร์ตัวใหม่ถูกเขียนด้วย Rust ก็คาดว่าน่าจะมี unsafe มากกว่า rustc เพราะโครงสร้างที่ใช้แคชและใช้อินเด็กซ์แทน pointer
      เราไม่ได้ตั้งใจจะลดทอนคุณค่าของงานของทีม Rust เพียงแต่อยากอธิบายการตัดสินใจและความคืบหน้าของเรา พร้อมกับให้ความเคารพต่อผลงานของโปรเจกต์อื่น
  • เป็นบทความที่ประเมินข้อดีข้อเสียของ Rust และ Zig อย่างสมดุลโดยไม่ปิดบัง และข้อสรุปที่ซื่อตรงเป็นพิเศษคือ ต่อให้หลังจากเขียนโค้ดไปหลายแสนบรรทัดตลอด 18 เดือนและจัดการบั๊กไปหลายร้อยรายการแล้ว แต่ถ้าเลือกภาษาอื่นในตัวเลือก ผลลัพธ์ของโปรเจ็กต์ก็คงไม่ต่างไปมากนัก
    อย่างไรก็ตาม คำอธิบายว่าการทำงานกับ หน่วยความจำแบบไม่ปลอดภัย เป็นส่วนใหญ่ของงานในคอมไพเลอร์ที่สร้างโค้ดเครื่องอย่าง Roc หรือ rustc นั้นเข้าใจได้ยาก
    มีคอมไพเลอร์จำนวนมากที่เขียนด้วย OCaml หรือ Haskell อยู่แล้ว และการสร้างโค้ดเครื่องเองก็เป็นเพียงการประกอบไบต์ลงในเวกเตอร์แล้วเขียนเป็นไฟล์ จึงไม่มีเหตุผลว่าทำไมต้องไม่ปลอดภัย
    ถ้าเป็นการแปลความหรือ JIT compilation ก็พอเข้าใจได้ แต่ก็ยังสงสัยว่าเหตุใดจึงจำเป็นแม้แต่กับการคอมไพล์ทั่วไป

    • การสร้างโค้ดเครื่องเองนั้นเป็นเพียงการเขียนไบต์ลงไป ดังนั้นคำพูดว่าไม่ปลอดภัยจึงไม่แม่นยำ
      ความเสี่ยงเกิดขึ้นตอนนำโค้ดเครื่องที่สร้างแล้วไปรัน และในทางปฏิบัติคอมไพเลอร์จำนวนมากก็มักสร้างและรันโค้ดเครื่องไปพร้อมกัน จึงใช้คำว่าเป็น ส่วนใหญ่ของงาน
      นอกจากการแปลความและ JIT แล้ว ยังรวมถึงงานอย่างการประเมินโค้ดของผู้ใช้ในช่วงคอมไพล์ เช่น const fn ของ Rust หรือ expression ที่ยกขึ้นระดับบนสุดได้ใน Roc ตลอดจนการรันเทสต์แล้วตรวจผลลัพธ์เพื่อกำหนดว่าจะนำอะไรไปแสดงให้ผู้ใช้เห็น
    • ดูเหมือนว่าจะหมายความว่า หากต้องการสร้าง คอมไพเลอร์แบบ incremental ที่เร็วมากอย่างคอมไพเลอร์ของ Zig ก็จำเป็นต้องใช้ภาษาระดับล่างและงานที่ไม่ปลอดภัยพอสมควร
    • ในบทความนี้ กรณีที่เกิดบั๊กด้าน memory safety จากการคอมไพล์ผิดพลาดก็ถูกนับเป็น บั๊กด้าน memory safety ของคอมไพเลอร์ ด้วย
      ไม่ว่าหน่วยความจำที่เสียหายจะอยู่ในโปรเซสของคอมไพเลอร์หรือในโปรแกรมที่ถูกสร้างขึ้น สาเหตุที่โปรเซสเซอร์ทำงานผิดก็ยังเป็นคำสั่งที่คอมไพเลอร์สร้าง และจุดที่ต้องแก้ก็ยังเป็นโค้ดของคอมไพเลอร์ตามเกณฑ์นี้
    • Roc ใช้ Zig กับรันไทม์และไลบรารีมาตรฐานอยู่แล้ว แม้ในช่วงที่ยังเขียนคอมไพเลอร์ด้วย Rust
      คอมโพเนนต์อย่างโค้ดลิงก์ภายนอกและ 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 ไม่ใช่เหตุผลเดียวของการเปลี่ยนภาษา

 
GN⁺ 4 시간 전
ความคิดเห็นจาก Hacker News
  • โดยรวมบทความก็โอเค แต่ยากจะเห็นด้วยกับคำกล่าวที่ว่า ในคอมไพเลอร์ที่สร้างโค้ดเครื่องอย่าง Roc หรือ rustc นั้น งานที่ไม่ปลอดภัยต่อหน่วยความจำ เป็นส่วนใหญ่ของงาน
    การแพตช์ไบนารีระหว่างรันหรือการโหลดโค้ดใหม่อาจต้องใช้โค้ดที่ไม่ปลอดภัย แต่ในกระบวนการสร้างไฟล์ปฏิบัติการตามปกติ การสร้างโค้ดเครื่องเองไม่ได้มีเหตุผลที่จะต้องไม่ปลอดภัย
    ตรงกันข้าม มีแนวโน้มจะพบโค้ดที่ไม่ปลอดภัยใน รันไทม์ ของภาษามากกว่า

    • เห็นด้วย การสร้างโค้ดเครื่องก็เป็นเพียงการเขียนไบต์เท่านั้น และความไม่ปลอดภัยที่อาจเกิดขึ้นจะเกิดตอนนำโค้ดเครื่องนั้นไปรัน
      อย่างไรก็ตาม ในทางปฏิบัติคอมไพเลอร์จำนวนมากสร้างโค้ดเครื่องแล้วรันมันโดยตรงด้วย จึงใช้คำว่า “เป็นส่วนใหญ่ของงาน” แต่ไม่ได้แปลว่าคอมไพเลอร์จำเป็นต้องทำทั้งสองอย่างเสมอไป
      ที่นึกถึงมีทั้งการแพตช์ไบนารี การโหลดโค้ดใหม่ รันไทม์ ตลอดจน การประเมินโค้ดของผู้ใช้ตอนคอมไพล์ เช่น const fn ของ Rust หรือ expression ที่ Roc สามารถย้ายขึ้นไปไว้ระดับบนสุดได้ รวมถึงการรันเทสต์แล้วตรวจผลลัพธ์เพื่อกำหนดสิ่งที่จะแสดง
    • คนจำนวนมากชอบบิดประเด็นจากข้อเท็จจริงที่ว่าแม้แต่ภาษาแบบ memory-safe ก็ยังมีบล็อกโค้ดที่ไม่ปลอดภัยอยู่ แล้วสรุปว่า “ถ้าอย่างนั้นความปลอดภัยของหน่วยความจำจะมีประโยชน์อะไร”
      คล้ายกับตรรกะที่ว่าต่อให้คาดเข็มขัดนิรภัยก็ยังหัวกระแทกได้ ดังนั้นเข็มขัดนิรภัยมีแต่ความน่ารำคาญและไม่ควรใช้
    • แม้งานที่ไม่ปลอดภัยจะไม่ได้จำเป็นโดยเนื้อแท้สำหรับการสร้างโค้ดเครื่อง แต่ผมคิดว่าตรงนี้สะท้อน ลำดับความสำคัญที่ต่างกัน
      ในโค้ดที่เน้นประสิทธิภาพสุดขั้ว โครงสร้างข้อมูล อัลกอริทึม และกลยุทธ์การจัดสรรหน่วยความจำย่อมต่างออกไป และ 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...

    • ในฐานะคนที่เขียนโค้ด Zig มาเยอะ ต่อให้มีฟีเจอร์แบบนั้นจริง มันก็ไม่เคยทำงานได้ดีในโค้ดของผม
      นี่เป็นความเห็นที่ได้จากการเขียนโค้ดเองก่อนยุค AI ไม่ใช่จากการให้ AI ช่วย และแม้การจับคู่ Zig กับ LLM อาจทำให้เกิดอะไรที่ต่างออกไป แต่ยิ่งอ่านก็ยิ่งเห็นคำกล่าวที่เขียนไว้แปลก ๆ จนเชื่อถือได้ยาก
      มันอ่านแล้วเหมือนบทความที่เขียนขึ้นเพื่อทำให้ข้อถกเถียงเดิมดูสมเหตุสมผลมากกว่าจะเป็นการตัดสินทางเทคนิคอย่างตรงไปตรงมา แต่เพราะผมชอบบทความและภาษาที่แปลกดี และก็ไม่ชอบกระแส AI ที่เกินจริงอยู่แล้ว เลยพยายามมองในแง่ดีไว้ก่อน
    • เท่าที่รู้ ReleaseSafe แค่เพิ่ม การตรวจขอบเขต และทำให้เกิด panic เมื่อไปถึงโค้ดที่ไม่ควรเข้าถึง
      Zig ดูเหมือนจะไม่ได้ให้ temporal memory safety
  • น่าสนใจที่แม้ OCaml จะสุกงอม ยืดหยุ่น และมีพลังในการแสดงออกพอสำหรับการพิสูจน์ต้นแบบ แต่ก็ไม่ได้ถูกเลือกเป็นภาษาสำหรับการใช้งานขั้นสุดท้าย
    ก็ยังสงสัยว่า incremental build ของ Zig จะเร็วกว่า dune แบบมีนัยสำคัญจริงหรือไม่ และแม้ cross-compilation จะเป็นข้อดี แต่ในส่วน “ทำไมต้อง Zig” กลับไม่ได้พูดถึง
    อยากรู้เหมือนกันว่าการควบคุมหน่วยความจำอย่างละเอียดสำคัญกับคอมไพเลอร์ขนาดนั้นจริงไหม และเหมือนกรณี Rust กับ WASM ที่เริ่มจาก OCaml ผู้ดูแลโครงการมักตัดสินใจเปลี่ยนภาษาในหมุดหมายแบบไหน

    • Rust เลิกใช้ OCaml ตอนตัดสินใจ เขียนใหม่ด้วย Rust เอง บทความนี้ก็สื่อว่า นั่นเป็นจังหวะที่พบได้บ่อยสำหรับการเขียนทั้งโปรเจกต์ใหม่ ซึ่งผมเห็นด้วยกับมุมมองนั้น
    • หนึ่งในเป้าหมายหลักของ Roc คือ ความเร็วของคอมไพเลอร์ และเพราะ OCaml ไม่ใช่ภาษา system programming จึงเดาว่าน่าจะถูกตัดออกจากตัวเลือกด้วยเหตุนี้
  • incremental build ของ Zig เป็นฟีเจอร์ชี้ขาดอย่างไม่ต้องสงสัย และก็เข้าใจได้หากจะยอมเปลี่ยนภาษาเพื่อให้ได้สิ่งนี้ในระยะสั้น
    แต่ในระยะกลางก็หวังว่า Rust จะมีฟีเจอร์คล้ายกันเพิ่มเข้ามาในอนาคตอันใกล้
    ผมอยากได้ความเร็ว แต่ก็ไม่อยากรีบแล้วมาสร้างปัญหาใหญ่ให้ตัวเอง และกำลังทำภาษาเองที่รวมความปลอดภัยแบบ Rust ฟีเจอร์แบบ Zig และรันไทม์แบบ Go ที่ไม่มี garbage collection

    • เรื่อง Rust build ที่เร็วขึ้นกำลังทำอยู่แล้วที่ https://rust-lang.github.io/rust-project-goals/2026/roadmap-... และเป้าหมายส่วนใหญ่ในหน้านี้ก็ตั้งไว้สำหรับปีนี้
    • มีโอกาสสูงที่ เวลาคอมไพล์ของ Rust จะเร็วขึ้นก่อนที่ Zig จะปลอดภัยขึ้นมากเสียอีก
    • แทนที่จะรอให้คอมไพเลอร์ Rust เร็วขึ้น ผมสงสัยว่าไปทางเพิ่ม borrow checker แบบหนึ่งให้ Zig จะเป็นอย่างไร
      แนวทางนี้ดูมีโอกาสเป็นจริงมากกว่า และน่าจะทำได้จากฝั่ง user space ด้วย
    • ถ้าไม่นับ garbage collection แล้ว ข้อดีพิเศษของ รันไทม์ Go คืออะไรบ้าง
    • ผมเอา Codex token ที่เหลือ ๆ ไปใช้กับ https://github.com/ityonemo/clr เป็นครั้งคราว เพื่อพยายามรวมความปลอดภัยแบบ Rust เข้ากับฟีเจอร์ของ Zig
  • ถ้าหากมีการเปรียบเทียบเชิง วิทยาศาสตร์ อย่างแท้จริงในกระบวนการเลือกภาษาที่ใช้พัฒนาคอมไพเลอร์ ก็น่าจะโน้มน้าวได้มากกว่านี้
    ดูเหมือนว่าจะเริ่มจากสมมติฐานที่ยังไม่ได้รับการพิสูจน์ว่าคอมไพเลอร์ประสิทธิภาพสูงจำเป็นต้องใช้ภาษาระบบระดับล่าง(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 ในโค้ดฝั่งไคลเอนต์หรือไม่

    • โดยเฉพาะกับภาษาที่ไม่คุ้นเคยอย่างภาษา functional ผมชอบดูว่ามันถูกนำไปใช้กับแอปแบบไหน แต่ต่อให้อ่าน roc-lang.org และ FAQ แล้วก็ยังจับภาพกรณีใช้งานที่เป็นรูปธรรมของ Roc ไม่ได้
  • เวลาในการคอมไพล์ เป็นปัจจัยที่ถูกประเมินค่าต่ำอย่างมาก การต้องรอบิลด์ C++ ครั้งละ 10 นาทีคือความไม่พอใจที่สุด และมันตัดสมาธิการพัฒนาไปอย่างสิ้นเชิง

    • ผมใช้ Rust-Analyzer ใน VS Code เพื่อการวิเคราะห์ไวยากรณ์และ autocomplete แต่ทุกครั้งที่เซฟไฟล์มันจะรันคอมไพเลอร์เพื่ออัปเดตข้อมูล ทำให้ช้ามากเกินไป
      แค่เปลี่ยนจากไฟล์ .rs ไปเป็นไฟล์ .ts ก็รู้สึกได้ถึงความต่างราวกับเปลี่ยนคอมพิวเตอร์ทั้งเครื่อง
  • แม้จะไม่ได้ใช้ Zig โดยตรง แต่ก็ยังคาดหวังกับความเป็นไปได้บางอย่างมาก
    โดยเฉพาะ เกมรูปแบบใหม่ ที่เขียนด้วย Zig ซึ่งใช้งานสะดวกกว่า C, ซอฟต์แวร์กระจายศูนย์ ที่ TigerBeetle แสดงให้เห็นศักยภาพแล้ว และสายงาน robotics ที่ผมสนใจเป็นการส่วนตัว