มีวิธีทำให้ความเร็วของ FFI ใน CRuby ดีขึ้นได้ไหม?
- เมื่อต้องเรียกใช้ native code จาก Ruby ควรเขียนโค้ด Ruby ให้มากที่สุดเท่าที่ทำได้ เพราะ YJIT สามารถปรับแต่งโค้ด Ruby ได้ แต่ไม่สามารถปรับแต่งโค้ด C ได้
- เมื่อต้องเรียกใช้ native library ควรทำงานส่วนใหญ่ใน Ruby และเขียน native extension ที่มี API แบบเรียบง่ายสำหรับการเรียก native function
- FFI ให้ประสิทธิภาพได้ไม่เท่ากับ native extension ตัวอย่างเช่น หากห่อฟังก์ชัน C อย่าง
strlen ด้วย FFI ประสิทธิภาพจะด้อยกว่าเมื่อเทียบกับ C extension
ผลลัพธ์เบนช์มาร์ก
- การเรียก
String#bytesize โดยตรงเร็วที่สุด และสามารถมองเป็นค่าฐานอ้างอิงได้
- การเรียก
strlen ผ่าน C extension เร็วเป็นอันดับสอง และการเรียก String#bytesize ทางอ้อมตามมาเป็นอันดับถัดไป
- การทำงานแบบ FFI ช้าที่สุด ซึ่งแสดงให้เห็นว่าการเรียก native function ผ่าน FFI มีโอเวอร์เฮดค่อนข้างมาก
จะเปลี่ยนความจริงข้อนี้ได้ไหม?
- จากไอเดียของ Chris Seaton กำลังสำรวจความเป็นไปได้ในการสร้างโค้ด JIT สำหรับการเรียก external function
- ในตัวอย่าง FFI wrapper เมื่อมีการเรียก
attach_function ก็สามารถสร้าง machine code ที่จำเป็นได้ตั้งแต่ตอนนิยาม wrapper function
การใช้ RJIT
- RJIT เป็นคอมไพเลอร์ JIT ที่เขียนด้วย Ruby และมาพร้อมกับ Ruby
- มีการแยก RJIT ออกเป็น gem เพื่อให้คอมไพเลอร์ JIT ของ 3rd party สามารถแมปกับโครงสร้างข้อมูลของ Ruby ได้ง่ายขึ้น
- มีการเรียกใช้ JIT entry function pointer อยู่เสมอ เพื่อให้ JIT ของ 3rd party สามารถลงทะเบียนกับ machine code ได้
การพิสูจน์แนวคิด
- ผ่านการพิสูจน์แนวคิดขนาดเล็กชื่อ "FJIT" สามารถสร้าง machine code ขณะรันไทม์เพื่อเรียก external function ได้
- จากผลเบนช์มาร์ก machine code ที่ FJIT สร้างขึ้นเร็วกว่า C extension และเร็วกว่า FFI call มากกว่า 2 เท่า
บทสรุป
- สิ่งนี้แสดงให้เห็นถึงความเป็นไปได้ในการเขียนโค้ด Ruby ให้มากที่สุดเท่าที่ทำได้ ขณะยังคงความเร็วระดับเดียวกับ C extension (หรือเร็วกว่าด้วยซ้ำ)
- Ruby อาจมีข้อดีในการเรียก native code ได้โดยไม่ต้องพึ่ง FFI
ข้อควรระวัง
- ขณะนี้จำกัดอยู่เฉพาะแพลตฟอร์ม ARM64 เท่านั้น และยังต้องเพิ่ม backend สำหรับ x86_64
- ยังไม่รองรับชนิดของพารามิเตอร์และชนิดค่าที่ส่งกลับทั้งหมด โดยรองรับได้เพียงพารามิเตอร์เดี่ยวและค่าที่ส่งกลับเดี่ยวเท่านั้น
- ต้องรัน Ruby ด้วยแฟล็ก
--rjit --rjit-disable ซึ่งน่าจะได้รับการแก้ไขเมื่อฟีเจอร์ของ Kokubun ถูกนำมาใช้
- ขณะนี้รันได้เฉพาะบน Ruby head เท่านั้น
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
เคยต้องจัดการ FFI อย่างมากเพื่อเรียกใช้ฟังก์ชันระหว่าง Java Constraint Solver (Timefold) กับ CPython
ด้วยบล็อกของ Rails At Scale และ byroot ตอนนี้จึงเป็นช่วงเวลาที่ดีในการสนใจการถกเถียงเชิงลึกเกี่ยวกับภายในของ Ruby และประสิทธิภาพ
คำถามว่าจะแทนการเรียกไลบรารี 3rd party สำหรับการเรียกฟังก์ชันภายนอกด้วยการ JIT คอมไพล์โค้ดได้หรือไม่
ข้อมูลเกี่ยวกับไลบรารีที่ใช้ JVMCI เพื่อสร้างโค้ด arm64/amd64 แบบทันทีและเรียกใช้ไลบรารีเนทีฟโดยไม่ต้องใช้ JNI: ลิงก์
ความเห็นที่ว่า "เขียน Ruby ให้มากที่สุดเท่าที่ทำได้ โดยเฉพาะเพราะ YJIT ปรับแต่งโค้ด Ruby ได้ แต่ทำแบบนั้นกับโค้ด C ไม่ได้"
ใช้ Ruby มานานกว่า 10 ปี และการได้เห็นพัฒนาการล่าสุดนั้นน่าสนใจมาก
สงสัยว่าทำไมจึงต้องใช้ JIT คอมไพล์
FFI - Foreign Function Interface หรือก็คือวิธีที่ Ruby ใช้เรียก C
คำถามว่านี่ไม่ใช่สิ่งที่ libffi ทำอยู่แล้วหรือ
ตอนนี้พอจะเข้าใจแล้วว่าทำไมถึงไม่ไปที่ tenderlovemaking.com