- YJIT และ ZJIT คือสถาปัตยกรรม JIT compiler ใน Ruby 3.x ที่ แปลงโค้ด Ruby เป็นภาษาเครื่องเพื่อเพิ่มความเร็วในการทำงาน
- YJIT จะ นับจำนวนครั้งที่มีการเรียกแต่ละฟังก์ชันหรือบล็อก และเมื่อถึงค่าขีดจำกัดที่กำหนด ก็จะแปลงโค้ดส่วนนั้นเป็นภาษาเครื่อง
- โค้ดที่แปลงแล้วจะถูกเก็บไว้ใน YJIT block โดยแต่ละบล็อกจะแปลง YARV instruction หลายรายการให้เป็น คำสั่งภาษาเครื่อง ARM64 ที่สอดคล้องกัน
- ใช้ Branch Stub เพื่อสังเกตชนิดข้อมูลจริงระหว่างรันไทม์ และเลือกสร้างคำสั่งภาษาเครื่องให้ตรงกับชนิดข้อมูลนั้นแบบเลือกสรร
- โครงสร้างนี้เป็นกลไกสำคัญในการทำให้ Ruby ได้ทั้งประสิทธิภาพการรันที่ดีขึ้นและการจัดการ dynamic type ที่มีประสิทธิภาพ ไปพร้อมกัน
Chapter 4: คอมไพล์ Ruby เป็นภาษาเครื่อง
Interpreting vs. Compiling Ruby Code
- ต้นฉบับไม่มีรายละเอียดเพิ่มเติม
การนับจำนวนการเรียกเมธอดและบล็อก
- YJIT ติดตาม จำนวนครั้งที่มีการเรียกฟังก์ชันและบล็อก ของโปรแกรมเพื่อระบุ โค้ดฮอตสปอต
- เก็บค่า jit_entry และ jit_entry_calls ไว้ข้างลำดับคำสั่ง YARV ของแต่ละฟังก์ชันหรือบล็อก
jit_entry จะเป็น null ในตอนแรก และภายหลังจะเก็บพอยน์เตอร์ไปยังโค้ดภาษาเครื่องที่ YJIT สร้างขึ้น
jit_entry_calls จะเพิ่มขึ้นทีละ 1 ทุกครั้งที่ถูกเรียก
- เมื่อจำนวนการเรียกถึงค่าขีดจำกัด YJIT จะ คอมไพล์โค้ดส่วนนั้นเป็นภาษาเครื่อง
- ค่าเริ่มต้นใน Ruby 3.5 คือ 30 ครั้งสำหรับโปรแกรมขนาดเล็ก และ 120 ครั้งสำหรับแอปพลิเคชันขนาดใหญ่
- เปลี่ยนได้ตอนรันด้วยออปชัน
--yjit-call-threshold
- ด้วยวิธีนี้ YJIT จะเปลี่ยนเฉพาะโค้ดที่ถูกรันบ่อยให้เป็นภาษาเครื่องเพื่อ สร้างเส้นทางการทำงานที่มีประสิทธิภาพ
YJIT Blocks
- YJIT เก็บคำสั่งภาษาเครื่องที่สร้างขึ้นไว้ใน YJIT block
- YJIT block แตกต่างจาก Ruby block และใช้แทน ช่วงย่อยบางส่วนของคำสั่ง YARV
- ฟังก์ชันหรือบล็อก Ruby แต่ละตัวประกอบด้วย YJIT block หลายบล็อก
- ในโปรแกรมตัวอย่าง เมื่อบล็อกถูกรันเป็นครั้งที่ 30 YJIT จะเริ่มคอมไพล์
- แปลงคำสั่ง YARV ตัวแรก
getlocal_WC_1 เป็นภาษาเครื่องและสร้าง YJIT block ใหม่
- จากนั้นคอมไพล์คำสั่ง
getlocal_WC_0 เพิ่มและรวมไว้ในบล็อกเดียวกัน
- ตาม Figure 4-8 YJIT จะสร้าง คำสั่ง ARM64 เพื่อโหลดค่าเข้ารีจิสเตอร์ x1 และ x9 ของโปรเซสเซอร์ M1
getlocal_WC_1 จะเก็บตัวแปรภายในจากสแตกเฟรมก่อนหน้า และ getlocal_WC_0 จะเก็บตัวแปรจากสแตกปัจจุบันลงในสแตก
- คำสั่งภาษาเครื่องที่สร้างขึ้นจะทำงานแบบเดียวกัน
YJIT Branch Stubs
- เมื่อ YJIT คอมไพล์คำสั่ง
opt_plus จะเกิด ปัญหาที่ไม่รู้ชนิดของ operand ล่วงหน้า
- ไม่ว่าจะเป็นจำนวนเต็ม สตริง หรือจำนวนทศนิยม ล้วนต้องใช้คำสั่งภาษาเครื่องต่างกันตามชนิดข้อมูล
- ตัวอย่าง: การบวกจำนวนเต็มใช้คำสั่ง
adds ส่วนการบวกจำนวนทศนิยมต้องใช้คำสั่งอีกแบบ
- เพื่อแก้ปัญหานี้ YJIT จึงใช้ การสังเกตระหว่างรันไทม์แทนการวิเคราะห์ล่วงหน้า
- ระหว่างที่โปรแกรมทำงาน จะตรวจสอบชนิดของค่าที่ถูกส่งเข้ามาจริง แล้วจึงสร้างโค้ดภาษาเครื่องที่เหมาะสม
- กลไกนี้ทำงานผ่าน Branch Stub
- เมื่อมีสาขา (branch) ใหม่ที่ยังไม่มีบล็อกเชื่อมต่ออยู่ จะเชื่อมไปยัง stub ชั่วคราวก่อน
- หลังจากทราบชนิดข้อมูลจริงแล้ว stub นั้นจะถูกแทนที่ด้วยบล็อกที่เหมาะสม
ZJIT (มีเพียงการกล่าวถึง)
- มีหัวข้อเกี่ยวกับ ZJIT อยู่ในสารบัญ แต่ไม่มีคำอธิบายโดยละเอียดในเนื้อหา
สรุป
- YJIT คือ JIT compiler ใน Ruby 3.5 ที่ออกแบบมาเพื่อเพิ่มประสิทธิภาพการรันของภาษาแบบ dynamic type
- หัวใจสำคัญคือ การทริกเกอร์การคอมไพล์ตามจำนวนครั้งที่ถูกเรียก, โครงสร้างแบบ YJIT block และ การตรวจสอบชนิดข้อมูลระหว่างรันไทม์ผ่าน Branch Stub
- มีการแปลงเป็นคำสั่งภาษาเครื่องจริงบนสถาปัตยกรรม ARM64 เพื่อ เพิ่มความเร็วในการรันโค้ด Ruby
- ZJIT ถูกกล่าวถึงในฐานะ JIT รุ่นถัดไป แต่ไม่มีรายละเอียดเพิ่มเติมในเนื้อหา
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เมื่อก่อนเคยมีช่วงที่ MacRuby คอมไพล์เป็น native code บน macOS ด้วย LLVM และผสานกับเฟรมเวิร์ก Objective‑C ได้
เป็นไอเดียที่เท่มากทีเดียว แต่สุดท้ายดูเหมือน Apple จะเปลี่ยนทิศไปทาง Swift
ถ้ามีเวอร์ชันใหม่ออกมา ฉันตั้งใจว่าจะซื้อหนังสือ Ruby Under a Microscope มาอ่านแน่นอน ยังชอบ Ruby อยู่เสมอ แต่ไม่ค่อยมีโอกาสได้ใช้จริงมากนัก
ตอนนี้มีคนอื่นรับช่วงต่ออยู่ แต่บรรยากาศเหมือนจะโฟกัสกับ DragonRuby มากกว่าในปัจจุบัน (เป็น Ruby implementation ที่เน้นเกม)
อ้างอิงได้จาก บทความวิกิ ด้วย
เพียงแต่ API รุ่นเก่าอาจไม่ได้รับการรองรับอีกแล้ว
VB6 นั้นพัฒนาได้เร็วมาก และยังทำงานกับ Direct3D หรือ ASP Classic ได้ด้วย
ความสง่างามและความสะดวกในการพัฒนา ของ Ruby ทำให้นึกถึงยุคนั้น
ถ้า Ruby มีเครื่องมือ GUI ระดับเดียวกับ VB6 ความนิยมของมันอาจต่างไปมากก็ได้
ดีใจมากที่เห็น Pat ยังเดินหน้ากับโปรเจ็กต์นี้ต่อ
หนังสือ Ruby Under a Microscope เล่มแรกและบทความในบล็อกของเขาเคยให้ แรงบันดาลใจ กับฉันมาก
เคยเจอเขาด้วยตัวเองที่งานประชุม Euruko ครั้งหนึ่ง เขาเป็นคนที่ยอดเยี่ยมจริง ๆ
ตอนที่อ่าน Ruby Under a Microscope ครั้งแรก สนุกมากจริง ๆ
ถึงขั้นเคยเอาไปใช้ตอน แก้โจทย์ CTF ด้วย
ช่วงหลังไม่ได้ตามรายละเอียดภายในของ Ruby แล้ว แต่ถ้ามีฉบับใหม่ออกมาก็ตั้งใจว่าจะซื้อแน่นอน
พอเห็นโพสต์นี้ก็อยากกลับไปอ่านหนังสือเวอร์ชันใหม่อีกครั้ง
พอพูดถึงการคอมไพล์ Ruby ก็เลยสงสัยว่าเคยลอง Sorbet compiler ที่นักพัฒนาของ Stripe สร้างไว้หรือยัง
โพสต์เปิดซอร์ส Sorbet Compiler
AOT compilation เป็นเรื่องที่ยากมากสำหรับ Ruby
สิ่งที่ทำให้แนวทางของ Sorbet น่าสนใจก็คือมันสามารถสร้าง fast path จากการ ตรวจสอบชนิดข้อมูล ของ Ruby ได้
ฉันเองก็กำลังทำ Ruby compiler เป็นโปรเจ็กต์ส่วนตัว โดยอ้างอิงจาก hokstad.com/compiler และ
writing-a-compiler-in-ruby
ตอนนี้กำลังโฟกัสที่การให้ผ่าน RubySpec อยู่ แล้วภายหลังก็คิดว่าจะลองทำ optimization บนพื้นฐานของชนิดข้อมูลด้วย
แม้จะไม่เกี่ยวกับการคอมไพล์ Ruby โดยตรง แต่หนังสือ Enterprise Integration with Ruby ให้ มุมมองเชิงลึก กับการใช้ Ruby ในงานนอกเหนือจากเว็บได้มากทีเดียว
ตั้งแต่รู้จัก MRuby ฉันก็สนุกกับการเปลี่ยนโปรเจ็กต์และสคริปต์ของตัวเองให้เป็นไฟล์ executable แบบแยกเดี่ยว
ดีใจที่ Ruby Under a Microscope ยังได้รับการอัปเดตอยู่เรื่อย ๆ
คิดว่าเป็น หนังสือที่ควรอ่าน สำหรับคนที่อยากเข้าใจการทำงานภายในของ Ruby
ฉันสงสัยว่าเวลาที่บล็อกของ YJIT ถูกเรียกหลายครั้ง มันติดตามการคอมไพล์แยกตามชนิดของอินพุตอย่างไร
อยากรู้ว่า Ruby จัดการกับชนิดต่าง ๆ อย่าง int หรือ float อย่างไร
มันใช้แนวทาง "wait‑and‑see" โดยเลื่อนการคอมไพล์ออกไปจนกว่าจะได้ชนิดจริงเข้ามา
จากนั้นก็เก็บเวอร์ชันของบล็อกแยกตามแต่ละชนิด แล้วเรียกใช้ให้เหมาะกับสถานการณ์
อัลกอริทึมนี้เรียกว่า Basic Block Versioning
Maxime Chevalier‑Boisvert จาก Shopify อธิบายไว้ดีมากใน วิดีโอบรรยาย RubyConf 2021
ส่วน ZJIT ซึ่งเป็น JIT engine ตัวใหม่ ดูเหมือนจะใช้วิธีที่ต่างออกไป
การทำให้ภาษาที่มี dynamic type เร็วขึ้นด้วย JIT มักต้องแลกกับ การใช้หน่วยความจำที่เพิ่มขึ้น
ถ้าไม่ใช่บริษัทใหญ่แบบ Shopify เรื่องนี้อาจกลายเป็นปัญหาที่หนักกว่าก็ได้
ทุกวันนี้ cloud instance มักให้หน่วยความจำราว 4GiB ต่อคอร์ ดังนั้น JIT code ระดับหลายร้อย MB ก็ยังถือว่ารับไหว
วิธีที่ YJIT หา hotspot ด้วยการนับแค่จำนวนครั้งที่มีการเรียกฟังก์ชันดูค่อนข้างเรียบง่าย
เลยสงสัยว่ามันมีความสามารถแบบ JavaScript JIT ที่ตรวจจับงานคำนวณหนักภายในลูปหรือเปล่า
โครงสร้างบล็อกของ Ruby อาจช่วยให้ทำ optimization แบบนี้ได้ด้วย
ทำให้ JIT สามารถมองบล็อกเป็นเหมือนฟังก์ชันแยกต่างหาก และ optimize การวนซ้ำได้อย่างเป็นธรรมชาติ
ประเด็นนี้จะพูดลึกขึ้นอีกในบทถัดไป