- โปรโตไทป์การยืมแบบ region ของ Vale คอมไพล์สำเร็จเป็นครั้งแรก ทำให้สามารถตรวจสอบแนวทางความปลอดภัยของหน่วยความจำที่ผสาน generational references กับ regions ได้ในโปรแกรมจริง
- นักพัฒนาสามารถเขียนโค้ดในสไตล์ใกล้เคียงกับ C/C++ และใช้ pure กับการยืมแบบ region เฉพาะจุดที่จำเป็น เพื่อลด overhead จากการตรวจสอบ generation
- โปรแกรม Vale แบบ zero-check ตัวแรกเป็นตัวอย่าง Cellular Automata สำหรับสร้างเลเวลเกม roguelike และ assembly ที่ได้ถูกปรับให้ใกล้เคียงกับโหมด
unsafe_with_bounds แทบทั้งหมด
- ใน benchmark,
safe_fastest ไม่พบ slowdown ที่สังเกตได้เมื่อเทียบกับ unsafe_with_bounds ส่วน unsafe_no_bounds เร็วกว่าสองโหมดนั้น 1.18 ± 0.01 เท่า
- นี่ยังไม่ใช่การเทียบโดยตรงกับ C/Rust และยังมีงานที่ต้องทำต่อ ได้แก่ pre-optimizer เฉพาะสำหรับ Vale และการจัดระเบียบฟีเจอร์ region เนื่องจาก noise จากการ optimize ของ LLVM, ความสมบูรณ์ของโปรโตไทป์ และการยังไม่รองรับ inline data
การผสาน generational references กับการยืมแบบ region
- แนวทางความปลอดภัยของหน่วยความจำของ Vale ตั้งเป้าไปในทิศทางที่ไม่ใช้ reference counting, tracing garbage collection หรือ borrow checking
- โครงสร้างพื้นฐานคือให้นักพัฒนาเขียนโปรแกรมในสไตล์ใกล้เคียงกับ C หรือ C++ แล้วให้ generational references ของ Vale รักษาความปลอดภัยของหน่วยความจำ
- จากนั้นเมื่อใช้ pure และ region borrowing ก็สามารถกำจัด overhead ส่วนใหญ่จากการตรวจสอบ generation ได้
- หากเพิ่ม linear style เข้าไปด้วย คาดว่าสามารถลดการตรวจสอบ generation ในโค้ด Vale ลงเหลือ zero ได้
- การยืมแบบ region เป็นแบบ opt-in ทั้งหมด จึงสามารถเขียนให้สะดวกก่อน แล้วค่อยเพิ่มเฉพาะส่วนที่ต้อง optimize ในภายหลังได้
- เป้าหมายคือโครงสร้างที่ภายในโปรแกรมเดียวกันสามารถเลือกให้บางส่วนยืดหยุ่นเหมือน Java, บางส่วนเร็วเหมือน Rust หรืออยู่ในจุดกึ่งกลางระหว่างนั้นได้
งานที่จำเป็นต่อการสร้างโปรโตไทป์
- ในช่วงหลายปีที่ผ่านมา มีการสร้างฐานของคอมไพเลอร์และทำให้รองรับทั้งระบบยืมตาม region กับ generational references ร่วมกัน
- ตัวระบบยืมเองซับซ้อนอยู่แล้ว อีกทั้งยังต้องใช้ full generics ที่ทรงพลังกว่า template เดิม
- เพื่อให้ regions และ generational references ทำงานร่วมกันได้อย่างเป็นธรรมชาติ จึงต้องมีขั้นตอนใหม่ในคอมไพเลอร์ด้วย
- ภายใน ระบบจะย่อ region ให้เป็นจำนวนเต็ม “pure height”
- region generic parameter แทนด้วยค่าลบ, region พื้นฐานคือ 0 และแต่ละ pure block แทนด้วยค่าบวกที่เพิ่มขึ้น
- เมื่อไม่กี่เดือนก่อน โปรโตไทป์ region เสร็จสมบูรณ์ และแม้ยังมีจุดที่หยาบอยู่บ้าง แต่ก็สามารถคอมไพล์บางอย่างได้สำเร็จเป็นครั้งแรก
- ผลลัพธ์คือ โปรแกรม Vale แบบ zero-check ตัวแรก
- สามารถนับจำนวนการตรวจสอบ generation ของโปรแกรมได้ด้วย flag คอมไพเลอร์
--print_mem_overhead true
โปรแกรม zero-check ตัวแรกและการเปรียบเทียบ assembly
- โปรแกรมแรกเป็นตัวอย่าง Cellular Automata สำหรับสร้างเลเวลเกม roguelike
- ความผิดพลาดเล็กน้อยในโค้ดคอมไพเลอร์ก็อาจใส่คำสั่งเพิ่มเติมลงใน assembly ที่ได้ และสร้าง overhead เทียมให้โปรแกรมสุดท้ายได้
- เพื่อติดตามปัญหา จึงเปรียบเทียบ assembly ที่ได้กับโหมด unsafe ของ Vale อย่างต่อเนื่อง
unsafe_no_bounds: ปิดการป้องกันความปลอดภัยของหน่วยความจำทั้งหมดคล้าย C และใช้ raw pointer แทน generational references
unsafe_with_bounds: เพิ่ม bounds checking ในการเข้าถึง array คล้าย Rust
- หลังจากติดตามความแตกต่างอยู่หลายเดือน assembly ที่ได้ก็เกือบเหมือนกับโหมด
unsafe_with_bounds
- ความแตกต่างที่คาดไว้เพียงอย่างเดียวคือการใส่ pseudo-random generation number ไว้ที่ส่วนบนของแต่ละ allocation และในการตรวจสอบ generation จริงก็ไม่ได้ถูกอ่าน
- ภายในใช้ register ที่เพิ่มขึ้นแบบ monotonically เพื่อรักษาความเร็ว
- หากเพิ่ม isolates หรือ unique references เข้ามา ก็สามารถกำจัดความแตกต่างนี้ได้
ผล benchmark และเงื่อนไขการวัด
- สรุป benchmark เป็นดังนี้
Summary
'./build_unsafe_no_bounds/main' ran
1.18 ± 0.01 times faster than './build_unsafe_with_bounds/main'
1.18 ± 0.01 times faster than './build_safe_fastest/main'
safe_fastest ซึ่งเป็น normal mode ของ Vale ไม่แสดง slowdown เมื่อเทียบกับโหมดที่มีเพียง bounds checking
- ในการวัดนี้ แนวทางดังกล่าวมี overhead ที่สังเกตได้เป็นศูนย์
- หากต้องการลองรันเอง สามารถ build regions branch, ดู benchmarking scripts และถามคำถามใน discord server ได้
- เงื่อนไขการวัดมีข้อจำกัดที่ชัดเจน
- ไม่ใช่ benchmark ที่เปรียบเทียบโดยตรงกับภาษาอย่าง C หรือ Rust
- คอมไพเลอร์ของภาษาเหล่านั้นมีการ optimize แยกกันมาหลายปี ซึ่งอาจทำให้ตัวแปรในการทดลองปะปนกัน
- เพื่อแยกความแตกต่างของแนวทางความปลอดภัยหน่วยความจำ จึงเปรียบเทียบกับ
unsafe_no_bounds และ unsafe_with_bounds
- สภาพแวดล้อมการวัดคือ Razer Blade 15" 2018, SSD 512GB ที่รัน Ubuntu 22.04
- เครื่องมือวัดคือ hyperfine และรันภายใน cset shield
noise จากการ optimize ที่พบในโปรแกรมขนาดใหญ่
- ในโปรแกรมที่ใหญ่ขึ้น พบ optimizer noise ค่อนข้างมาก
- ต่างจาก benchmark noise เพราะการตั้งค่าการวัดให้เวลา runtime ที่สอดคล้องมาก เช่น
± 0.01
- การเปลี่ยนแปลงเล็กน้อยในบริเวณหนึ่งสามารถทำให้ค่าที่วัดแกว่งไปทางใดทางหนึ่งได้
- มีกรณีที่เมื่อเปลี่ยนขนาดของ generation number แล้วได้ negative overhead อย่างสม่ำเสมอที่
1.13 ± 0.01
- เป็นผลลัพธ์ที่แปลก เพราะในโปรแกรมไม่ได้มี generation number มากนัก
- เป็นไปได้ว่าการเปลี่ยนแปลงของ register allocation กลบความต่างด้านประสิทธิภาพที่มาจากความแตกต่างทางความหมาย
- ในโปรแกรมที่ใหญ่กว่าอย่างเกม roguelike ขนาดเล็ก optimizer ไม่สามารถรวม branch สองอันที่เหมือนกันภายใน if statement ได้ และยังพลาดการ optimize ที่เห็นได้ชัดอื่น ๆ ด้วย
- ยังไม่แน่ชัดว่าการมีอยู่ของ integer ที่ไม่ได้ถูกอ่านส่งผลอย่างไร และอาจเป็น bug ของ LLVM ก็ได้
- ผลลัพธ์นี้ชี้ว่าอาจจำเป็นต้องมี pre-optimizer เฉพาะสำหรับ Vale คล้ายกับ MIR ของ Rust
- LLVM ถูกออกแบบโดยคำนึงถึง C มากกว่า
- หาก LLVM มอง generational references เป็นรูปแบบการเข้าถึงหน่วยความจำที่ถูก free ไปแล้วโดยเจตนา ก็อาจถือเป็น undefined behavior ได้
ความสามารถในการนำไปใช้และงานถัดไป
- เมื่อผสาน generational references กับ regions เข้าด้วยกัน จะสามารถสร้างแนวทางความปลอดภัยของหน่วยความจำที่เร็วมากได้
- โดเมนซอฟต์แวร์ที่แนวทางนี้อาจเหมาะสมมีเงื่อนไขดังนี้
- ต้องการ latency ที่คาดการณ์ได้มากกว่า tracing garbage collection
- ต้องการประสิทธิภาพและ cache friendliness ที่ดีกว่า reference counting
- ต้องการ prototype และ iterate ได้ง่ายกว่า borrow checking
- ก่อนที่ Vale จะถูกนำไปเทียบตรง ๆ กับ C หรือ C++ ยังมีงานที่เหลืออยู่
- LLVM optimizer มีปัญหาในการอนุมาน generation และ immutability จึงจำเป็นต้องมี pre-optimizer เฉพาะสำหรับ Vale
- Vale ต้องรองรับ inline data แทนทางแก้ชั่วคราวในปัจจุบันที่วาง struct ทั้งหมดไว้บน heap
- benchmark ข้างต้นไม่ได้ใช้ struct ดังนั้นการยังไม่รองรับ inline data จึงไม่ส่งผลต่อผลลัพธ์นี้
- ฟีเจอร์ region ยังอยู่ในขั้น prototype จึงต้องขัดเกลาส่วนที่ยังหยาบและลดหนี้ทางเทคนิคก่อนจะ merge เข้า main branch
- หลังจาก merge แล้ว มีแผนจะทำให้ standard library ใช้ regions เพื่อให้โค้ด main program ของผู้ใช้ได้ประโยชน์โดยไม่ต้องใช้ regions โดยตรง
- การวัดปัจจุบันแสดงให้เห็นว่าโปรแกรมแบบ zero-check เป็นไปได้ และสามารถไปถึงความเร็วตามที่คาดหวัง
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ผมลองดาวน์โหลด Vale มาทดลองใช้ แต่ความประทับใจแรกไม่ดีเลย เพราะตอนรันคอมไพเลอร์
valecครั้งแรกโดยไม่ใส่อาร์กิวเมนต์ มันพิมพ์ว่า "(panic)" ออกมาทันทีpanicเป็นคำที่แรงมาก และผมคิดว่าควรหลีกเลี่ยงในสถานการณ์การจัดการข้อผิดพลาดตามปกติ การที่โปรแกรมเกิด panic ทำให้รู้สึกเหมือนเป็นสถานการณ์ที่ควบคุมไม่ได้ เลยรู้สึกค้างคาไม่ดีจากนั้นผมพยายามดู help ของอาร์กิวเมนต์บรรทัดคำสั่ง แต่ตอนนี้แทบไม่มีเลย พอเซฟตัวอย่าง Hello World จากเว็บไซต์ลงใน
hello.vlแล้วรันvalec hello.vlก็ได้ข้อความUnknown subcommandผมเลยรัน
valec build hello.vlก็ได้Unrecognized input: hello.vlตามด้วย(panic)อีกครั้ง และvalec helpก็ไม่ได้ช่วยอะไร สุดท้ายเลยยอมแพ้ ไม่รู้ว่าควรใช้มันยังไงcatไฟล์valec-help-build.txtที่รวมมากับไฟล์ดาวน์โหลดโดยตรง น่าจะเจอคำอธิบายที่ต้องการตอนนี้คอมไพเลอร์ยังมีความหยาบอยู่มาก ตั้งแต่สิงหาคมถึงพฤษภาคม เราโฟกัสกับการทำต้นแบบ region แบบ 100% และสิ่งที่คุณเจอตอนนี้คือหนี้ทางเทคนิคที่สะสมจากช่วงนั้น รวมถึงการที่ระบบ help ไม่มี integration test ด้วย
ตลอด 1–2 เดือนที่ผ่านมาเรากำลังทยอยชำระหนี้นั้น แต่ยังกลับไปถึงระดับตอนรีลีส 0.2 ไม่ได้ ถ้าต้องการความช่วยเหลือเพิ่มเติม บอกเราได้ หรือเข้ามาที่เซิร์ฟเวอร์ Discord ก็มีคนช่วยได้เยอะ
แต่ README ไม่ได้สื่อเรื่องนี้ให้ชัดเจน และเขียนว่า “Try Vale” เลยทำให้ก้ำกึ่ง ถึงอย่างนั้นตอนนี้ก็ดูใกล้เคียงงานวิจัยและพัฒนา/พิสูจน์แนวคิดมากกว่า
แม้มองในแง่อินเทอร์เฟซผู้ใช้หรือบั๊ก ประสบการณ์ดีบัก C++ อายุ 40 ปีด้วย gdb อายุ 35 ปี ก็ยังสู้กับภาษาเชิงทดลองไหน ๆ ได้สบาย เช่น การพิมพ์
funcname()::staticvarnameเป็นอินเทอร์เฟซที่ประหลาด และประมาณครึ่งหนึ่งก็ล้มเหลวอยู่ดี ไม่ต้องพูดถึงระบบ build ของ C++ถ้าเป็นเทคโนโลยีเชิงทดลอง เราอาจวิจารณ์แนวคิดได้ แต่ผมคิดว่าอินเทอร์เฟซผู้ใช้ที่ยังหยาบอยู่เป็นสิ่งที่พอรับได้ในระดับหนึ่ง
https://github.com/ValeLang/Vale#building-a-vale-program
บอกว่ามี latency ที่คาดเดาได้มากกว่า tracing garbage collection, มีประสิทธิภาพและเป็นมิตรกับแคชมากกว่า reference counting, และทำ prototype กับ iterate ได้ง่ายกว่า borrow checking แบบนี้จากแค่สงสัยก็กลายเป็นสนใจขึ้นมาเลย
ผมเริ่ม subscribe RSS feed ด้วย: https://verdagon.dev/rss.xml
Vale ต้องการ ผู้สนับสนุน เพิ่ม
https://github.com/sponsors/ValeLang
ระหว่างที่โพสต์นี้อยู่หน้าแรก ผมหวังว่าจะช่วยให้โปรเจกต์ไปถึงเป้าหมาย 3,000 ดอลลาร์ต่อเดือนได้
ผมอยากช่วยให้ Evan ทำงานนี้แบบเต็มเวลาได้ ผมเองก็เป็นผู้สนับสนุน ภาษาที่เร็ว ปลอดภัย และยังสนุกกับการทำ prototype คุ้มค่าที่จะสนับสนุน
“pre-optimizer เฉพาะของ Vale คล้ายกับ Cranelift ของ Rust” น่าจะหมายถึง MIR หรือก็คือ mid-level intermediate representation มีบล็อกโพสต์ดี ๆ ที่เกี่ยวข้องอยู่: https://blog.rust-lang.org/2016/04/19/MIR.html
Cranelift เป็น compiler backend ที่เน้น JIT เป็นหลัก แต่ในทางทฤษฎีก็อาจใช้แทน LLVM ได้ งาน backend ทางเลือกก็กำลังดำเนินอยู่ แต่มีข้อจำกัด: https://github.com/bjorn3/rustc_codegen_cranelift
แนวทางที่ทำให้โค้ดส่วนใหญ่ไม่ต้องสนใจการจัดการหน่วยความจำ แต่ยังให้ทางเลือกในการ optimize เฉพาะ hot code path ด้วย zero-cost abstraction ฟังดูเหมือนได้ข้อดีของทั้งสองฝั่ง
โดยเฉพาะถ้าเงื่อนไขคือแลกแค่ performance ไม่ใช่ safety เพื่อความสะดวก
shared ownership เป็นแนวคิดที่แย่ เลยไม่ใช้ smart pointer ด้วย
ปัญหาการจัดการหน่วยความจำโดยมากแล้วเป็นเรื่องเล็กน้อย
ยังสงสัยอยู่เรื่อย ๆ ว่าคำว่า ปลอดภัย ในบริบทของ generational references หมายถึงอะไร
ถ้าเข้าใจถูก มันหมายถึงการป้องกัน use-after-free กับ double-free ใช่ไหม? ถ้าอย่างนั้นเมื่อ generation ที่คาดไว้กับ generation จริงไม่ตรงกัน โปรแกรมก็ยังอาจล้มเหลวตอนเข้าถึงหน่วยความจำได้
ในแง่นั้นมันดูปลอดภัยน้อยกว่า reference counting, tracing garbage collection และ borrow checking
ถ้าพยายามเข้าถึงหน่วยความจำที่ถูก free ไปแล้วผ่าน reference ก็ควรได้ segmentation fault หรือ assertion failure ที่คาดเดาได้และปลอดภัย ในอนาคตถ้ามีการปรับปรุงให้ remap พื้นที่ virtual address space ได้ ก็อาจกำจัด segmentation fault ได้ด้วย ซึ่งผมตั้งตารออยู่
แต่ถ้าเป็น generational index runtime ก็น่าจะตรวจได้ว่าการเข้าถึงนั้น valid หรือไม่ก่อนจะพยายามเข้าถึงจริง ๆ ไม่รู้ว่าใน Vale ทำได้หรือเปล่า
malloc/freeและมีข้อดีอื่น ๆ ด้วยฟังก์ชัน
checkต้องใช้ generation number ของ allocation จึงต้องเข้าถึง allocation นั้น กล่าวคือ เพื่อจะตรวจว่า reference เข้าถึง allocation นั้นได้ไหม ก็ต้องเข้าถึง allocation นั้นก่อนอยู่ดีแน่นอนว่าถ้า allocation ถูก free ไปแล้ว การเข้าถึง allocation นั้นและ generation number เองก็เป็น undefined behavior อยู่แล้ว จึงใช้ไม่ได้
มันดูชัดเจนเกินไปจนไม่รู้ว่าผมพลาดอะไรใหญ่ ๆ ไปหรือเปล่า หรือว่า “memory safety” ที่พูดถึงตรงนี้หมายถึงคนละอย่างกันโดยสิ้นเชิง
Vale ไม่ใช่ภาษาเดียวกับ V. V เคยถูกรีวิวอย่างวิจารณ์รุนแรงมากใน https://mawfig.github.io/2022/06/18/v-lang-in-2022.html แต่เพราะชื่อคล้ายกัน ผมเลยจำผิดว่าเป็น Vale
ฝากไว้เผื่อมีคนทำพลาดแบบเดียวกัน
เนื้อหาในบทความตอนนี้ไม่เกี่ยวข้องแล้ว แต่ก็ยังคงค้างอยู่ และยังเป็นบทความเดียวของบล็อกนั้นด้วย
ตอนนี้เป็นปี 2023 แล้ว และ V ก็อยู่ใน beta (0.4) ด้วย ยิ่งกว่านั้นคนที่ทำบทความนั้นใช้บัญชี GitHub แบบใช้แล้วทิ้ง โพสต์รีวิว/โจมตีจนเกิดประเด็น แล้วก็หายไป
บทความเดียวที่ลงในบล็อกนั้นก็มีแต่บทความโจมตี V ไม่มีรีวิวอื่น ส่วนที่มีเนื้อหาจริง ๆ ก็ถูกแก้ไปแล้ว[1]
ถ้าค้นหา
mawfig.githubก็จะเห็นว่าถูกเอาไปโปรยซ้ำ ๆ บน HN และมักใช้เพื่อโจมตีให้เสียหาย[1]: https://github.com/vlang/v/issues/14803
[1]: https://github.com/vlang/v/issues/14787
[1]: https://github.com/vlang/v/issues/14786
ขอแสดงความยินดีกับ Evan ที่บรรลุ milestone นี้ แม้ผมจะไม่มีประสบการณ์ด้านการออกแบบภาษาโปรแกรมหรือคอมไพเลอร์ แต่ก็ชอบอ่าน บทความของ Vale
ตอนนี้มีทั้ง Vale ของ Evan และ Val ของ Adobe Software Technology Lab ทำให้การค้นหาเอกสารที่เกี่ยวข้องน่าจะค่อนข้างยากขึ้น
https://www.val-lang.dev
“Vale เร็ว: Vale ถูก AOT compile ด้วย LLVM, ใช้ static typing, ใช้เทคนิค generational references แบบใหม่เพื่อ memory safety ที่ทั้งเร็วและยืดหยุ่น และจะเร็วขึ้นอีกในเร็ว ๆ นี้ด้วยการนำ region borrow checking มาใช้”
https://vale.dev/
รู้สึกเหมือนแอบฟังการโต้เถียงของคนสองคนที่ดำเนินมา 5 ปี
มีใครอธิบายได้ไหมว่าตรงนี้เกิดอะไรขึ้น? บทความอ่านยากเกินไป
สรุปคือ Vale เป็นภาษาที่เหมือน C++ เวอร์ชันสะอาดกว่า และใช้ generational references[0] ซึ่งในเชิงแนวคิดคล้ายกับการเปิด ASan[1] แล้วรัน
generational references มี overhead อยู่บ้าง แต่สามารถกำจัดได้ด้วย regions[2] หรือให้เจาะจงกว่านั้นคือ immutable region borrowing[3] สิ่งนี้ทำให้ Vale เข้าใกล้เป้าหมายการเป็นภาษาประสิทธิภาพสูงที่ยังคง memory safety
[0] https://verdagon.dev/blog/generational-references
[1] https://github.com/google/sanitizers/wiki/AddressSanitizer
[3] https://verdagon.dev/blog/zero-cost-borrowing-regions-overvi...
[4] https://verdagon.dev/blog/zero-cost-borrowing-regions-part-1...