Spinel - คอมไพเลอร์เนทีฟแบบ AOT สำหรับ Ruby
(github.com/matz)- คอมไพเลอร์แบบ AOT ที่ทำ การอนุมานชนิดข้อมูลทั้งโปรแกรม จากซอร์ส Ruby แล้วแปลงเป็นโค้ด C เพื่อ สร้างไบนารีเนทีฟแบบสแตนด์อโลน
- พัฒนาโดย matz ผู้สร้าง Ruby โดยตรง และตัวคอมไพเลอร์แบ็กเอนด์เองก็เขียนด้วย Ruby เป็นสถาปัตยกรรมแบบ self-hosting ที่ คอมไพล์ตัวเองได้
- ประสิทธิภาพ เร็วขึ้นประมาณ 11.6 เท่า เมื่อเทียบกับ miniruby (Ruby 4.1.0dev), Conway's Game of Life เร็วขึ้น 86.7 เท่า, ackermann 74.8 เท่า, mandelbrot 58.1 เท่า
- ไปป์ไลน์การคอมไพล์จะแปลง Ruby เป็น ข้อความ AST ด้วยพาร์เซอร์ที่อิง Prism ก่อน จากนั้นแบ็กเอนด์แบบ self-hosting จะทำการอนุมานชนิดและสร้างโค้ด C แล้วใช้คอมไพเลอร์ C มาตรฐานสร้างไบนารีแบบ standalone
- รองรับฟีเจอร์ Ruby ได้กว้างขวาง เช่น class, inheritance, block, การจัดการข้อยกเว้น, Fiber, เอนจิน Regexp แบบ NFA ในตัว, Bigint แบบยกระดับอัตโนมัติ, pattern matching เป็นต้น
- คลาสขนาดเล็กที่มีฟิลด์สเกลาร์ไม่เกิน 8 ฟิลด์จะถูก จัดสรรบนสแตกอัตโนมัติเป็น value type ทำให้ตัด GC overhead ได้ทั้งหมด (การจัดสรร 1 ล้านครั้ง 85ms → 2ms)
- ทำ flatten การเชนสตริง
a + b + c + dด้วย malloc เพียงครั้งเดียว และsplitภายในลูปจะ นำ sp_StrArray กลับมาใช้ซ้ำ เพื่อตัดการจัดสรรที่ไม่จำเป็น - มีการเพิ่มประสิทธิภาพตอนคอมไพล์หลายอย่าง เช่น hoisting ความยาวที่ไม่เปลี่ยนในลูป, constant propagation, inline เมธอดอัตโนมัติเมื่อมีไม่เกิน 3 statement, และจบการอนุมานแบบวนซ้ำก่อนกำหนด (ช่วยลดเวลา bootstrap ได้ราว 14%)
- ไบนารีที่สร้างขึ้น ไม่มี runtime dependency เลย, สามารถรันได้ด้วยเพียง libc + libm
- ไม่รองรับ
eval, metaprogramming, Thread และ Mutex เป็นต้น (รองรับเฉพาะ Fiber) - ไลเซนส์ MIT
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ถ้าเป็นสิ่งที่ Matz ทำเอง ก็น่าจะเข้าใจข้อจำกัดของ Ruby semantics ดีอยู่แล้ว เลยรู้สึกเชื่อถือได้
วิทยานิพนธ์ปริญญาโทของผมก็เป็น AOT JS compiler เหมือนกัน มันใช้งานได้จริง แต่ข้อจำกัดของข้อมูลขาเข้ามีเยอะมาก สุดท้ายเลยพับไป
ตอนนั้นนักพัฒนา JS ยังไม่ค่อยคุ้นกับการรักษาข้อจำกัดเหล่านั้นด้วยตัวเอง และอินพุตที่ไม่อาจรู้ล่วงหน้าได้โดยเนื้อแท้อย่าง
JSON.parseก็เป็นอุปสรรคสำคัญตอนนี้ด้วย TypeScript มันอาจเป็นเรื่องที่ทำได้จริงมากกว่าสมัยนั้นมาก
แค่ดู lambda calculus ทั่วไปก็จะเห็นชัดว่าการอนุมานชนิดข้อมูลมีขีดจำกัด และในงานของ Matt Might หรือโปรเจ็กต์ Shed-skin Python ก็มีข้อจำกัดคล้ายกันโผล่มาเหมือนกัน
ผมสงสัยว่า
eval,send,method_missing,define_methodพบได้บ่อยแค่ไหนในโค้ด Ruby จริง ๆ และสงสัยด้วยว่าปกติจัดการกับการพาร์สแบบไม่มีชนิดข้อมูล เช่น อินพุต JSON กันอย่างไรการพาร์ส Ruby ยากจนแทบจะยากกว่าการแปลเสียอีก เลยใช้ Prism และผลลัพธ์คือสร้าง C ออกมา
ตัว Ruby semantics พื้นฐานจริง ๆ ไม่ได้ยากขนาดนั้นที่จะอิมพลีเมนต์
ตรงกันข้าม ผมกำลังง่วนอยู่กับ self-hosting AOT compiler เก่าที่เขียนด้วย Ruby ล้วน และดันยืนกรานจะใช้ parser ของตัวเอง เลยเลือกเส้นทางที่ยากกว่ามากโดยตั้งใจ
ผมเรียนรู้ตั้งแต่เนิ่น ๆ ว่า 80% แรกนั้นทำแบบคร่าว ๆ ก็ยังรันโค้ด Ruby ได้เยอะพอสมควร แต่ “80% ที่สอง” ที่ยากจริง ๆ กลับไปกองอยู่กับสิ่งที่ Matz ตัดออกจากโปรเจ็กต์นี้และ mruby เช่น encoding หรือฟีเจอร์จิปาถะรอบข้างสารพัด
พูดตามตรง Ruby มีฟีเจอร์อยู่ไม่น้อยที่ผมไม่เคยเห็นในโค้ดจริงเลยสักครั้ง ดังนั้นถ้าบางอย่างจะ deprecated ก็คงไม่แปลก
send,method_missing,define_methodเจอบ่อยมากข้อจำกัดก็คล้าย mruby และถึงมีข้อจำกัดแบบนั้นก็ยังมีที่ให้ใช้งานอยู่
การรองรับ
send,method_missing,define_methodค่อนข้างทำได้ไม่ยากแต่การรองรับ eval() นั้นทรมานมาก
อย่างไรก็ตาม สัดส่วนใหญ่ของ
eval()ใน Ruby สามารถลดรูปแบบคงที่ให้เป็น instance_eval แบบ block version ได้ และในกรณีนั้น AOT compile ก็จะง่ายขึ้นมากตัวอย่างเช่น ถ้ารู้สตริงที่ใส่เข้า
eval()แบบคงที่ได้ หรือสามารถแยกวิเคราะห์มันได้ ก็มีทางแก้ได้เยอะในทางปฏิบัติ การใช้
eval()จำนวนมากนั้นไม่จำเป็นหรือแทบเป็นแค่การอ้อมข้อจำกัดของ introspection แบบง่าย ๆ ซึ่งจัดการได้ด้วยการตรวจสอบแบบสถิตคอมไพเลอร์ของผมเองก็คิดว่าจะไปลงมือที่จุดนั้นก่อนถ้ามันกลายเป็นคอขวด
การรับ JSON แบบไม่มีชนิดข้อมูลก็น่าจะใช้กลไกพวกนี้เหมือนกัน
ถ้าตัดสิ่งเหล่านั้นออกไป ก็จะเหลือภาษาที่เล็กและอ่านง่าย ซึ่งแม้จะไม่ strong type เท่า Crystal แต่ก็ไม่ได้พึ่ง metaprogramming หนักเท่า Ruby ทางการ
เพราะงั้นศักยภาพก็ดูสูงพอสมควร แต่สุดท้ายก็คงต้องรอเวลาเป็นตัวตัดสิน
evalบ่อยจะไม่ใช้ก็ได้แหละ แต่สำหรับผมแบบนั้นมัน ergonomic กว่า
eval,exec,define_methodและการสร้างคลาสใหม่ด้วยClass.new,Struct.newการใช้งานส่วนใหญ่ของพวกนี้มักกระจุกอยู่ตอน boot แอป หรือระหว่าง require ไฟล์ และในแง่หนึ่งมันก็คล้ายขั้นตอนคอมไพล์อยู่แล้ว
นี่คือสิ่งที่ Matz เพิ่งประกาศในงาน RubyKaigi 2026
แม้จะยังเป็นงานทดลอง แต่ก็ทำขึ้นในเวลาประมาณหนึ่งเดือนด้วยความช่วยเหลือจาก Claude และเดโมสดก็สำเร็จด้วย
ชื่อนี้มาจากแมวตัวใหม่ของ Matz และชื่อแมวนั้นก็มาจากชื่อแมวใน Card Captor Sakura ซึ่งต่อจากนั้นก็ไปจับคู่กับตัวละครที่ชื่อ Ruby อีกที
สำหรับคนอย่าง Matz มันอาจเหมือนดันจาก 100x ไปเป็น 500x เลยก็ได้
https://en.wikipedia.org/wiki/Spinel
ดูเหมือนว่าวิดีโอยังไม่ขึ้นเป็นไลฟ์ และน่าจะกำลังทยอยอัปขึ้นช่องนี้ทีละคลิป
https://www.youtube.com/@rubykaigi4884/videos
แม้แต่ชื่อโปรเจ็กต์เองก็ให้ความรู้สึกเหมือนตั้งจากอารมณ์
เห็นได้ชัดว่าน่าประทับใจมาก แต่ก็ดูเหมือนจะบำรุงรักษาไม่ได้เลยถ้าไม่มี AI agent
spinel_codegen.rbยาว 21,000 บรรทัด และบางเมธอดซ้อนลึกถึง 15 ชั้นโค้ดคอมไพเลอร์เดิมทีก็ยากที่จะเขียนให้สวยอยู่แล้ว แต่ชิ้นนี้ดูยากมากเป็นพิเศษสำหรับให้คนดูแลต่อ
คอมไพเลอร์มีเส้นแบ่ง subsystem ชัดเจน และ handoff ตามแต่ละขั้นก็ค่อนข้างชัด จึงเป็นงานประเภทที่ modular ได้ง่ายที่สุดอย่างหนึ่งด้วยซ้ำ
ปัญหามักอยู่ที่ทำให้มันรันได้ก่อนแล้วไม่มีเวลา refactor หลังจากนั้น พอเป็นแบบนั้นความเละก็จะพอกพูนต่อไปเรื่อย ๆ
spinel_codegen.rbนี่แทบระดับ eldritch horror เลยเวลาใช้ Claude ผมก็ได้ spaghetti code แบบนี้ออกมาตลอด เลยเคยสงสัยว่าตัวเองทำอะไรผิดหรือเปล่า
แต่พอเห็นว่าแม้แต่โปรเจ็กต์ที่น่าสนใจจริง ๆ จากคนที่ผมมองว่าเป็นโปรแกรมเมอร์ระดับท็อปก็ยังมีคุณภาพโค้ดแย่เป็นหย่อม ๆ เหมือนกัน ก็รู้สึกว่าไม่ได้เป็นอยู่คนเดียว
อย่าง
infer_comparison_type()ยังไม่ใช่กรณีเลวร้ายสุด และก็ไม่ได้อ่านยากอะไร แต่จริง ๆ มีวิธีเขียนที่ง่ายและชัดเจนกว่านี้มาก ทว่า Claude กลับไปไม่ถึงถ้ารวม comparison operator ไว้ใน
Setแล้วใช้include?จัดการ ก็จะสั้นกว่า เร็วกว่า อ่านง่ายกว่า และดูแลง่ายกว่าแต่ Claude มักจะไหลไปทาง สาย if-return ต่อเนื่อง เสมอ แถมให้ความรู้สึกว่าไม่ค่อยคุ้นแม้แต่กับ if-else ด้วย
โค้ดเบสที่ Claude เขียนให้ผมก็เต็มไปด้วยแพตเทิร์นแบบนั้น และตอนนี้ก็รู้แล้วว่าไม่ได้เป็นกับผมคนเดียว
ในทางกลับกัน ไฟล์อื่น ๆ ดีกว่ามาก โดยเฉพาะไดเรกทอรี
libที่ดูเหมือนจะสอดคล้องกับไดเรกทอรีextของ Ruby repo หลัก และคุณภาพก็ถือว่าดีตัว API ก็ได้รับอิทธิพลจาก MRI Ruby อย่างชัดเจน และแม้อิมพลีเมนเทชันจะแตกต่างอยู่มาก แต่เหมือน Matz จะชี้นำให้บางส่วนของ API ต้นฉบับยังคงหน้าตาคล้ายเดิม เลยทำให้ผลลัพธ์ดูเป็นระเบียบขึ้น
[1] https://github.com/matz/spinel/blob/98d1179670e4d6486bbd1547...
ขอแค่ผ่านทั้งการทดสอบและ benchmark ผมก็พอใจแล้ว
แต่ไฟล์ขนาดมหึมาแบบนี้ AI เองจะจัดการได้ง่ายหรือเปล่าก็ยังน่าสงสัย
ผมพยายามจำกัดไฟล์ไม่ให้เกินประมาณ 300 บรรทัด และคิดว่าโค้ดที่คนเข้าใจง่ายก็น่าจะง่ายสำหรับ coding agents ด้วยเหมือนกัน
ข้อจำกัดมีดังนี้
No eval:
eval,instance_eval,class_evalNo metaprogramming:
send,method_missing,define_method(แบบไดนามิก)No threads:
Thread,Mutex(รองรับ Fiber)No encoding: สมมติว่าเป็น UTF-8/ASCII
No general lambda calculus:
-> x { }แบบซ้อนลึกพร้อมการเรียก[]สำหรับผม การสมมติว่าเป็น UTF-8/ASCII ไม่ใช่ข้อจำกัดใหญ่ แต่ที่เหลือน่าจะเป็นข้อจำกัดจริงสำหรับโปรแกรมจำนวนมากพอสมควร
และถ้าจะใส่กลับเข้าไป ก็ดูเหมือนต้องใช้แรงงานอีกมาก
ผมใช้ Ruby มานาน และจากมุมของคนที่เคยใช้ฟีเจอร์ทั้งหมดที่ไล่มานั้น จริง ๆ แล้วสิ่งที่ผมอยากได้หลังผ่านการคัดสรรตามธรรมชาติกลับเป็น Ruby แบบเรียบง่าย อย่างนี้
มันง่ายกว่า เข้าใจง่ายกว่า แต่ยังคงสุนทรียะแบบ Ruby เอาไว้
ตอนนี้ด้วย LLM ประสิทธิภาพการสร้างโค้ดสูงมาก จนไม่จำเป็นต้องลด boilerplate ด้วย metaprogramming เพื่อเพิ่ม productivity แบบเมื่อก่อนอีกแล้ว
เพราะสัดส่วนที่นักพัฒนาต้องเขียนโค้ดเองจริง ๆ ก็น้อยลงอยู่แล้ว
ไวยากรณ์คล้ายกันและมี static type system จึงต่อยอดไปสู่โค้ดที่คอมไพล์แล้วมีประสิทธิภาพมากกว่า
evalผมยังมองว่าอาจดีกว่า แต่การไม่มี threads และ mutexes ด้วยนี่น่าเสียดายการไม่มี
define_methodก็พอเข้าใจได้เมื่อคิดถึงการใช้งานของมันแต่
sendกับmethod_missingนั้นพบได้บ่อยในไลบรารีเดิม และก็ดูไม่น่าจะยากมากที่จะอิมพลีเมนต์ด้วยการสร้าง memory lookup table ตั้งแต่ตอนคอมไพล์เลยไม่แน่ใจว่านี่เป็นสิ่งที่จงใจตัดออก หรือแค่ยังไปไม่ถึงตรงนั้น
ผมหวังว่าอย่างหลัง แต่ไม่ว่าอย่างน้อยตอนนี้ก็คงยังเอาไปใช้จริงในงานได้ยากเพราะปัญหา compatibility
แต่มันคือ การลดปริมาณโค้ดที่ต้องอ่าน
นี่เจ๋งมากจริง ๆ และผมรอ AOT compiler สำหรับ Ruby มานานแล้ว
แค่เสียดายที่ไม่มี fallback สำหรับ
evalหรือ metaprogramming แต่ก็ดูเหมือนตั้งใจโฟกัสที่ subset เล็ก ๆ ที่ประสิทธิภาพสูงผมหวังว่า gem ที่สร้างด้วย AOT compiler นี้จะทำงานร่วมกับ MRI ได้ดี
ฝั่งการแพ็กหรือบันเดิล Ruby มาตรฐานกับ gem นั้นยังคงต้องพึ่ง tebako, kompo, ocran และในอดีตก็เคยมีโปรเจ็กต์อย่าง ruby-packer, traveling ruby, jruby warbler
การมีตัวเลือกเพิ่มมาอีกหนึ่งอย่างถือว่าดี แต่ผมก็ยังอยากเห็น เวอร์ชันตัดสินเกม ที่มี UX ฝั่งนักพัฒนาดีกว่านี้
เพราะมันไม่ได้อัปเดตมานานเกินไปแล้ว
ผมสงสัยว่าทำไมถึง no threads
Ruby scheduler กับ pthread implementation ข้างใต้ก็ดูเหมือนน่าจะทำงานได้ดีในฝั่ง C อยู่แล้ว หรือว่าตั้งใจจะทำแบบ zero dependency
ถ้าไม่ได้มีแผนจะใส่เป็น optional extension ทีหลัง หรือไม่ได้แค่ยังไม่ทันทำ การเลือกแบบนี้ก็ดูแปลกอยู่เหมือนกัน
น่าจะเป็นแค่ว่ายังทำไปไม่ถึงตรงนั้นมากกว่า
การทำ multithreading ให้ถูกต้องนั้นเดิมทีก็ยากมากอยู่แล้ว
น่าทึ่งที่ทำได้ในเวลาแค่เดือนเศษ
จะพูดเรื่อง AI ยังไงก็ตาม ถ้าอยู่ในมือของ นักพัฒนาที่มีฝีมือ มันสร้างแรงเร่งความเร็วได้มหาศาลจริง ๆ
แต่ Matz ให้ความรู้สึกว่าแค่
gem env|infoกับfindก็พอแล้วในเมื่อ Matz เป็นคนทำเอง ก็สงสัยว่ามีโอกาสจริงจังแค่ไหนที่สิ่งนี้จะกลายเป็นส่วนหนึ่งของ Ruby core ในอนาคต
แล้วถ้าเป็นแบบนั้น มันจะเป็นภัยต่อ Crystal มากแค่ไหนด้วย
คุณลักษณะพวกนี้แทบจะจำเป็นสำหรับการคอมไพล์และดูแลโปรแกรมขนาดใหญ่
ขณะที่สิ่งนี้รองรับแค่ subset ของ Ruby ที่มีข้อจำกัด ดังนั้น gem ยอดนิยมส่วนใหญ่ของ Ruby ก็น่าจะรันตรง ๆ ไม่ได้
ในแง่ที่มันเป็น subset ของภาษาที่มุ่งสู่การคอมไพล์เป็น C มันดูใกล้กับ PreScheme มากกว่า
ณ ตอนนี้ผมยังไม่มองว่าทั้งสองอย่างกำลังแข่งกันตรง ๆ ในพื้นที่เดียวกัน
Ruby แบบสมบูรณ์นั้นแทบแน่นอนว่ายังต้องพึ่ง JIT
[1]: https://prescheme.org/
มันเหมือนเป็นการล้างแค้นของเครื่องมืออย่าง Rational Unified Process กับ Enterprise Architect
ต่างกันแค่ว่าแทนที่จะได้ UML diagram เราจะได้ไฟล์ markdown มาแทน
สิ่งนี้น่าจะมีประโยชน์ในฝั่ง infrastructure tools
เช่น อาจจินตนาการถึง bundler ที่เขียนด้วย Ruby แต่คอมไพล์แบบสถิตได้ จนทำหน้าที่เป็นเครื่องมือติดตั้ง Ruby แบบ RVM ไปด้วยในตัว
buildpack ของ Ruby แบบเดิมก็เขียนด้วย Ruby แต่ต้อง bootstrap ด้วย bash ทำให้ชวนหงุดหงิดและเกิด edge case ตามมา
CNB เขียนด้วย Rust เพื่อเลี่ยงปัญหานั้น และแนวคิดเรื่องการแจกจ่ายเป็น single binary แบบไร้ dependency นั้นทรงพลังมากจริง ๆ