Spinel: คอมไพเลอร์เนทีฟ AOT สำหรับ Ruby
(github.com/matz)- แปลงโค้ด Ruby ให้เป็น ไบนารีเนทีฟแบบสแตนด์อโลนที่รันได้เอง และมุ่งให้รันได้เร็วกว่า CRuby
minirubyรุ่นล่าสุดโดยเฉลี่ยเรขาคณิตราว 11.6 เท่า ผ่านการทำ type inference ระดับทั้งโปรแกรมและการสร้างโค้ด C - ไปป์ไลน์การคอมไพล์เริ่มจากพาร์เซอร์ที่อิง Prism แปลง Ruby เป็น ข้อความ AST จากนั้นแบ็กเอนด์แบบ self-hosting จะทำ type inference และสร้างโค้ด C ก่อนคอมไพล์เป็นไบนารีสแตนด์อโลนด้วยคอมไพเลอร์ C มาตรฐาน
- แบ็กเอนด์ของคอมไพเลอร์มีโครงสร้าง self-hosting ที่เขียนด้วย Ruby และหลังผ่านกระบวนการ bootstrap จะได้
gen2.c == gen3.cทำให้ลูปการคอมไพล์ตัวเองซ้ำปิดสมบูรณ์ - ใส่ การปรับแต่งตอนคอมไพล์ เช่น string concatenation flattening, value-type promotion, loop-invariant length hoisting, static symbol interning และ bigint auto-promotion พร้อมมีเอนจิน regexp ในตัว, bigint และรันไทม์แบบ single-header เพื่อลดการพึ่งพารันไทม์ภายนอก
- แม้จะยังไม่รองรับ
eval, metaprogramming, Thread และการจัดการ encoding ทั่วไป แต่ก็แสดงให้เห็นถึงความเป็นไปได้เชิงปฏิบัติของการคอมไพล์ Ruby แบบ AOT จากรูปแบบการแจกจ่ายที่ รันได้โดยไม่ต้องมี Ruby และช่องว่างด้านประสิทธิภาพที่ชัดเจนในงานคำนวณเข้มข้น
วิธีการทำงาน
- ไปป์ไลน์การคอมไพล์ประกอบด้วยการพาร์สไฟล์ Ruby แล้วซีเรียลไลซ์เป็น ไฟล์ข้อความ AST จากนั้นทำ type inference และสร้างโค้ด C ก่อนสร้างเป็นไบนารีเนทีฟด้วยคอมไพเลอร์ C มาตรฐาน
spinel_parseใช้ Prism และ libprism ในการพาร์ส Ruby และถ้ายังไม่มีไบนารี C จะใช้เส้นทางสำรองผ่าน CRuby และ Prism gemspinel_codegenทำงานเป็นไบนารีเนทีฟแบบ self-hosted โดยรับ AST แล้วทำ type inference + สร้างโค้ด C- ขั้นสุดท้ายคือคอมไพล์ซอร์ส C พร้อมรันไทม์เฮดเดอร์ด้วย
cc -O2 -Ilib -lmและได้ผลลัพธ์เป็นไบนารีแบบ standalone
Self-Hosting
- สาย bootstrap ทำงานโดยใช้
CRuby + spinel_parse.rbเพื่อสร้าง AST แล้วใช้CRuby + spinel_codegen.rbเพื่อสร้างgen1.cและbin1จากนั้นใช้ไบนารีที่สร้างขึ้นมาสร้างgen2.cและgen3.cต่อจนลูปปิดสมบูรณ์ - มีการระบุว่า
gen2.c == gen3.cซึ่งยืนยันว่าลูป bootstrap loop ปิดแล้ว - แบ็กเอนด์
spinel_codegen.rbถูกเขียนด้วยส่วนย่อยของ Ruby ที่ Spinel คอมไพล์ได้เอง- classes,
def,attr_accessor if/case/whileeach/map/select,yieldbegin/rescue- การทำงานกับ String, Array, Hash และ File I/O
- classes,
- แบ็กเอนด์ไม่มี metaprogramming,
eval,require
ประสิทธิภาพและเบนช์มาร์ก
- การทดสอบผ่าน 74 รายการ และเบนช์มาร์กผ่าน 55 รายการ
- ค่าเฉลี่ยเรขาคณิตจากเบนช์มาร์ก 28 รายการเร็วกว่า CRuby
minirubyรุ่นล่าสุดประมาณ 11.6 เท่า - เกณฑ์เปรียบเทียบคือบิลด์ CRuby
minirubyรุ่นล่าสุดที่ไม่มี bundled gem และแม้เทียบกับฐานที่เร็วกว่าอย่างruby3.2.3 ของระบบ ก็ยังเหนือกว่าอย่างมากในเวิร์กโหลดคำนวณเข้มข้น -
ประสิทธิภาพด้านการคำนวณ
lifeเร็วกว่า 86.7 เท่า: 20ms เทียบกับ 1,733msackermannเร็วกว่า 74.8 เท่า: 5ms เทียบกับ 374msmandelbrotเร็วกว่า 58.1 เท่า: 25ms เทียบกับ 1,453ms- เวอร์ชันเรียกซ้ำของ
fibเร็วกว่า 34.2 เท่า: 17ms เทียบกับ 581ms nqueensเร็วกว่า 30.4 เท่า: 10ms เทียบกับ 304mstaraiเร็วกว่า 28.8 เท่า: 16ms เทียบกับ 461mstakเร็วกว่า 24.2 เท่า: 22ms เทียบกับ 532msmatmulเร็วกว่า 24.1 เท่า: 13ms เทียบกับ 313mssudokuเร็วกว่า 17.0 เท่า: 6ms เทียบกับ 102mspartial_sumsเร็วกว่า 16.1 เท่า: 93ms เทียบกับ 1,498msfannkuchเร็วกว่า 9.5 เท่า: 2ms เทียบกับ 19mssieveเร็วกว่า 8.5 เท่า: 39ms เทียบกับ 332msfastaเร็วกว่า 7.0 เท่า: 3ms เทียบกับ 21ms
-
โครงสร้างข้อมูลและ GC
rbtreeเร็วกว่า 22.6 เท่า: 24ms เทียบกับ 543mssplay treeเร็วกว่า 13.9 เท่า: 14ms เทียบกับ 195mshuffmanเร็วกว่า 9.8 เท่า: 6ms เทียบกับ 59msso_listsเร็วกว่า 5.4 เท่า: 76ms เทียบกับ 410msbinary_treesเร็วกว่า 3.6 เท่า: 11ms เทียบกับ 40mslinked_listเร็วกว่า 2.9 เท่า: 136ms เทียบกับ 388msgcbenchเร็วกว่า 2.0 เท่า: 1,845ms เทียบกับ 3,641ms
-
โปรแกรมจริง
json_parseเร็วกว่า 10.1 เท่า: 39ms เทียบกับ 394ms- การคำนวณ
bigint_fib1000 หลัก เร็วกว่า 8.0 เท่า: 2ms เทียบกับ 16ms ao_renderเร็วกว่า 8.0 เท่า: 417ms เทียบกับ 3,334mspidigitsเร็วกว่า 6.5 เท่า: 2ms เทียบกับ 13msstr_concatเร็วกว่า 6.5 เท่า: 2ms เทียบกับ 13mstemplate engineเร็วกว่า 6.2 เท่า: 152ms เทียบกับ 936mscsv_processเร็วกว่า 3.7 เท่า: 234ms เทียบกับ 860msio_wordcountเร็วกว่า 2.9 เท่า: 33ms เทียบกับ 97ms
ความสามารถของ Ruby ที่รองรับ
- ฟีเจอร์ Core รองรับ classes, inheritance,
super, mixin แบบinclude,attr_accessor,Struct.new,alias, module constants และ open classes สำหรับ built-in types - Control Flow รองรับ
if/elsif/else,unless,case/when, pattern matching แบบcase/in,while,until,loop,for..in,break,next,return,catch/throw,&. - Blocks รองรับ
yield,block_given?,&block,proc {},Proc.new,-> x { },method(:name)รวมถึงเมธอดที่ใช้บล็อกอย่างeach,map,select,reduce,sort_by,times,upto,downto - Exceptions รองรับ
begin/rescue/ensure/retry,raiseและคลาส exception ที่ผู้ใช้กำหนดเอง - Types ครอบคลุม Integer, Float, String, Array, Hash, Range, Time, StringIO, File, Regexp, Bigint, Fiber
- ค่าที่มีหลายรูปแบบชนิดจัดการด้วย tagged unions
- มี nullable object types
T?สำหรับโครงสร้างข้อมูลแบบอ้างอิงตัวเอง
- Global Variables อย่าง
$nameจะถูกคอมไพล์เป็นตัวแปร C แบบ static และตรวจพบ type mismatch ได้ตั้งแต่ตอนคอมไพล์ - I/O รองรับ
puts,print,printf,p,gets,ARGV,ENV[],File.read/write/open,system(), แบ็กติก
สตริง, เรกซ์เพรสชัน, ซิมโบล, Bigint, Fiber
- Strings รองรับทั้งสตริง immutable และ mutable โดย
<<จะถูกยกระดับเป็นสตริง mutablesp_Stringอัตโนมัติเพื่อทำ append แบบ in-place ระดับ O(n) +, interpolation,tr,ljust/rjust/centerและเมธอดมาตรฐานทำงานได้กับทั้งสองรูปแบบของสตริง- การเปรียบเทียบอย่าง
s[i] == "c"ถูกปรับแต่งให้เข้าถึงอาร์เรย์ของ char โดยตรง จึงทำงานแบบ ไม่ต้องจัดสรรหน่วยความจำเพิ่ม - การต่อแบบ
a + b + c + dจะถูก flatten เหลือการเรียกsp_str_concat4หรือsp_str_concat_arrเพียงครั้งเดียว ทำให้มีการจัดสรรน้อยลง N-1 ครั้ง str.split(sep)ภายในลูปจะนำsp_StrArrayตัวเดิมกลับมาใช้ซ้ำ และในcsv_processทำให้ ตัดการจัดสรร 4 ล้านครั้ง- Regexp ใช้ NFA regexp engine ในตัวโดยไม่พึ่งพาภายนอก
- รองรับ
=~,$1-$9,match?,gsub,sub,scan,split
- รองรับ
- Bigint ใช้จำนวนเต็มความแม่นยำไม่จำกัดที่อิง mruby-bigint
- รูปแบบอย่าง
q = q * kในลูปจะถูกยกระดับอัตโนมัติ - ลิงก์เป็น static library และจะถูกรวมเข้ามาเมื่อมีการใช้งานจริงเท่านั้น
- รูปแบบอย่าง
- Fiber ให้ความสามารถด้าน concurrent แบบ cooperative บน
ucontext_t- รองรับ
Fiber.new,Fiber#resume,Fiber.yieldและการส่งผ่านค่า - ตัวแปรอิสระจะถูก capture เป็นเซลล์ที่ย้ายขึ้น heap
- รองรับ
- Symbols ถูกอิมพลีเมนต์เป็นชนิด
sp_symที่แยกจากสตริง- รักษาความหมาย
:a != "a" - symbol literal จะถูก intern ตอนคอมไพล์เป็นค่าคงที่
SPS_name String#to_symจะใช้ dynamic pool เมื่อจำเป็นเท่านั้น- hash ที่ใช้คีย์เป็น symbol ใช้
sp_SymIntHashเพื่อเก็บคีย์จำนวนเต็มโดยตรงแทนสตริง จึงไม่ต้องมีทั้ง strcmp และการจัดสรรสตริงแบบไดนามิก
- รักษาความหมาย
การจัดการหน่วยความจำและ value type
- การจัดการหน่วยความจำใช้ mark-and-sweep GC พร้อม size-segregated free lists, non-recursive marking และ sticky mark bits
- คลาสขนาดเล็กและเรียบง่ายจะถูกยกระดับเป็น value types อัตโนมัติและวางบนสแตก
- เงื่อนไขคือมีฟิลด์ scalar ไม่เกิน 8 ฟิลด์
- ไม่มี inheritance
- ไม่มีการเปลี่ยนแปลงผ่านพารามิเตอร์
- การจัดสรรคลาสที่มี 5 ฟิลด์จำนวน 1 ล้านครั้ง ลดเวลาจาก 85ms เหลือ 2ms
- โปรแกรมที่ใช้แต่ value types จะ ไม่ต้อง export รันไทม์ GC ออกมาเลย
การปรับแต่ง
- ทำ การปรับแต่งตอนคอมไพล์ หลายแบบโดยอิงจาก type inference ระดับทั้งโปรแกรม
- Value-type promotion ทำให้คลาส immutable ขนาดเล็กกลายเป็นออบเจ็กต์ C struct บนสแตก เพื่อตัดโอเวอร์เฮดของ GC
- Constant propagation ทำให้ค่าคงที่ลิเทอรัลธรรมดาอย่าง
N = 100ถูก inline ตรงที่ใช้งานโดยไม่ต้อง lookup ผ่านcst_N - Loop-invariant length hoisting ทำให้
while i < arr.lengthคำนวณความยาวเพียงครั้งเดียวก่อนเข้าลูป- แต่ถ้ามีการเปลี่ยนออบเจ็กต์ผู้รับในบอดี เช่น
arr.pushการ hoist นี้จะถูกปิดใช้งาน
- แต่ถ้ามีการเปลี่ยนออบเจ็กต์ผู้รับในบอดี เช่น
- Method inlining จะใส่
static inlineให้เมธอดสั้นที่ไม่เรียกซ้ำและมีไม่เกิน 3 statement เพื่อกระตุ้นให้ gcc inline - String concat chain flattening ลดสายการต่อให้เหลือการเรียกครั้งเดียว เพื่อตัดการสร้างสตริงกลาง
- Bigint auto-promotion ยกระดับรูปแบบการบวกแบบอ้างอิงตัวเองหรือการคูณซ้ำไปเป็น bigint โดยอัตโนมัติ
- Bigint
to_sใช้mpz_get_strของ mruby-bigint เพื่อทำงานแบบ divide-and-conquer ระดับ O(n log²n) - Static symbol interning เปลี่ยน
"literal".to_symให้เป็นค่าคงที่SPS_<name>ตั้งแต่ตอนคอมไพล์ และจะใส่รันไทม์พูลก็ต่อเมื่อใช้ dynamic interning - ใน
sub_rangeสตริงที่มีความยาวถูก hoist แล้วจะใช้sp_str_sub_range_lenเพื่อข้ามการเรียกstrlenภายใน line.split(",")ภายในลูปจะใช้sp_StrArrayเดิมซ้ำ- Dead-code elimination ใช้
-ffunction-sections -fdata-sectionsและ--gc-sectionsเพื่อลบฟังก์ชันรันไทม์ที่ไม่ได้ใช้จากไบนารีสุดท้าย - Iterative inference early exit จะหยุดลูป fixed-point ทันทีถ้าอาร์เรย์ลายเซ็นของ param, return และ ivar ทั้ง 3 ชุดไม่เปลี่ยนอีกต่อไป
- โปรแกรมส่วนใหญ่คอนเวอร์จใน 1-2 รอบแทนที่จะต้องวนครบ 4 รอบ
- เวลา bootstrap ลดลงประมาณ 14%
parse_id_listbyte walk เปลี่ยนพาร์เซอร์รายการฟิลด์ AST ที่ถูกเรียกระหว่าง self-compile ราว 120,000 ครั้ง จากs.split(",")ไปเป็นการเดินด้วยมือผ่านs.bytes[i]ทำให้การจัดสรรต่อการเรียกลดจาก N+1 เหลือ 2 ครั้ง- โค้ด C ที่สร้างขึ้นยังคง คอมไพล์ได้โดยไม่มี warning ภายใต้ระดับ warning ปกติ และฮาร์เนสใช้
-Werrorเพื่อให้รีเกรสชันปรากฏทันที
สถาปัตยกรรม
- โครงสร้างรีโพซิทอรีแบ่งเป็นองค์ประกอบต่อไปนี้
spinel: สคริปต์แรปเปอร์คำสั่งเดียว บน POSIX shellspinel_parse.c: ฟรอนต์เอนด์ C จาก libprism ไปเป็นข้อความ AST จำนวน 1,061 บรรทัดspinel_codegen.rb: แบ็กเอนด์คอมไพเลอร์จาก AST ไปเป็นโค้ด C จำนวน 21,109 บรรทัดlib/sp_runtime.h: เฮดเดอร์ไลบรารีรันไทม์จำนวน 581 บรรทัดlib/sp_bigint.c: จำนวนเต็มความแม่นยำไม่จำกัด 5,394 บรรทัดlib/regexp/: เอนจิน regexp ในตัว 1,759 บรรทัดtest/: การทดสอบฟังก์ชัน 74 รายการbenchmark/: เบนช์มาร์ก 55 รายการMakefile: ระบบอัตโนมัติสำหรับการบิลด์
- รันไทม์
lib/sp_runtime.hรวม GC, การอิมพลีเมนต์ array/hash/string และส่วนสนับสนุนรันไทม์อื่น ๆ ไว้ใน ไฟล์เฮดเดอร์เดียว - โค้ด C ที่สร้างขึ้นจะ include เฮดเดอร์นี้ และลิงเกอร์จะดึงเฉพาะส่วนที่จำเป็นจาก
libspinel_rt.a- bigint
- regexp engine
- พาร์เซอร์มี 2 อิมพลีเมนเทชัน
spinel_parse.cลิงก์กับ libprism โดยตรงและทำงานได้ โดยไม่ต้องมี CRubyspinel_parse.rbเป็น CRuby fallback ที่ใช้ Prism gem
- พาร์เซอร์ทั้งสองสร้างผลลัพธ์ AST เหมือนกัน และแรปเปอร์
spinelจะเลือกใช้ไบนารี C ก่อนถ้าเป็นไปได้ require_relativeจะถูก resolve ตั้งแต่ตอนพาร์สและไฟล์ที่อ้างอิงจะถูก inline เข้ามา
ข้อจำกัด
- No eval: ไม่รองรับ
eval,instance_eval,class_eval - No metaprogramming: ไม่รองรับ
send,method_missing,define_methodแบบไดนามิก - No threads: ไม่รองรับ
Thread,Mutexและรองรับเฉพาะ Fiber - No encoding: สมมติว่าใช้ UTF-8 และ ASCII
- No general lambda calculus: ไม่รองรับ
-> x { }ที่ซ้อนลึกและการเรียก[]
การพึ่งพาและโมเดลการรัน
- การพึ่งพาตอนบิลด์คือไลบรารี C libprism และ CRuby สำหรับ bootstrap เริ่มต้น
- ไม่มีการพึ่งพาตอนรันไทม์ และไบนารีที่สร้างขึ้นต้องการเพียง libc + libm
- regexp ใช้เอนจินในตัวจึงไม่ต้องมีไลบรารีภายนอก
- Bigint ถูกรวมมาในตัว แต่จะลิงก์เข้ามาเมื่อมีการใช้งานจริงเท่านั้น
- Prism คือพาร์เซอร์ Ruby ที่
spinel_parseใช้งานmake depsจะดาวน์โหลด prism gem tarball จาก rubygems.org และแตกซอร์ส C ลงในvendor/prism- ถ้าติดตั้ง prism gem ไว้แล้ว ระบบจะตรวจพบโดยอัตโนมัติ
- สามารถกำหนดพาธเองได้ด้วย
PRISM_DIR=/path/to/prism
- CRuby จำเป็นเฉพาะในช่วง bootstrap แรกเท่านั้น และหลัง
makeแล้วไปป์ไลน์ทั้งหมดจะรันได้ โดยไม่ต้องมี Ruby
ประวัติโปรเจกต์
- Spinel เริ่มต้นจากการอิมพลีเมนต์ด้วย C มีขนาด 18K lines และยังคงอยู่ในสาขา
c-version - หลังจากนั้นมีการเขียนใหม่ด้วย Ruby ในสาขา
ruby-v1 masterปัจจุบันคือเวอร์ชันที่เขียนใหม่อีกครั้งด้วยส่วนย่อยของ Ruby ที่สามารถ self-hosting ได้
ไลเซนส์
- ใช้ MIT License
- เป็นไปตามไฟล์ LICENSE
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 นั้นทรงพลังมากจริง ๆ