2 คะแนน โดย GN⁺ 2025-02-14 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

มีวิธีทำให้ความเร็วของ 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 ความคิดเห็น

 
GN⁺ 2025-02-14
ความคิดเห็นใน Hacker News
  • เคยต้องจัดการ FFI อย่างมากเพื่อเรียกใช้ฟังก์ชันระหว่าง Java Constraint Solver (Timefold) กับ CPython

    • ปัญหาด้านประสิทธิภาพของ FFI ส่วนใหญ่เกิดจากการใช้พร็อกซีเพื่อสื่อสารระหว่างภาษาโฮสต์กับภาษาปลายทาง
    • การเรียก FFI โดยตรงด้วย JNI หรือ foreign interface แบบใหม่ทำได้รวดเร็ว และมีความเร็วใกล้เคียงกับการเรียกเมธอด Java โดยตรง
    • แต่ตัวเก็บขยะของ CPython และ Java เข้ากันได้ไม่ดี จึงต้องใช้เทคนิคพิเศษเพื่อซิงโครไนซ์
    • หากใช้พร็อกซีอย่าง JPype หรือ GraalPy จะมีโอเวอร์เฮดด้านประสิทธิภาพ เพราะต้องแปลงพารามิเตอร์และค่าที่ส่งกลับ และอาจทำให้เกิดการเรียก FFI เพิ่มเติม
    • เมื่อส่งอ็อบเจ็กต์ CPython ไปยัง Java ฝั่ง Java จะถือพร็อกซีของอ็อบเจ็กต์ CPython นั้น
    • หากส่งพร็อกซีนั้นกลับไปยัง CPython อีกครั้ง ก็จะเกิดพร็อกซีของพร็อกซีขึ้นมา
    • ผลลัพธ์คือพร็อกซีของ JPype ช้ากว่าการเรียก CPython ผ่าน FFI โดยตรง 1402% และพร็อกซีของ GraalPy ช้ากว่า 453%
    • สุดท้ายจึงแปลงไบต์โค้ดของ CPython เป็นไบต์โค้ดของ Java และสร้างโครงสร้างข้อมูล Java ที่สอดคล้องกับคลาส CPython ที่ใช้งาน
    • ผลคือได้ประสิทธิภาพดีขึ้น 100 เท่าเมื่อเทียบกับการใช้พร็อกซี
    • การแปลงหรืออ่านไบต์โค้ดของ CPython นั้นไม่เสถียรอย่างมากและมีเอกสารน้อย อีกทั้งยังแมปไปยังไบต์โค้ดอื่นโดยตรงได้ยากเพราะความแปลกเฉพาะของ VM หลายอย่าง
    • ดูรายละเอียดเพิ่มเติมได้ในบล็อกโพสต์: ลิงก์
  • ด้วยบล็อกของ Rails At Scale และ byroot ตอนนี้จึงเป็นช่วงเวลาที่ดีในการสนใจการถกเถียงเชิงลึกเกี่ยวกับภายในของ Ruby และประสิทธิภาพ

    • ด้วยการปรับปรุง Ruby และ Rails ในช่วงหลัง นี่เป็นช่วงเวลาที่ดีสำหรับคนที่ใช้ Ruby
  • คำถามว่าจะแทนการเรียกไลบรารี 3rd party สำหรับการเรียกฟังก์ชันภายนอกด้วยการ JIT คอมไพล์โค้ดได้หรือไม่

    • มั่นใจว่านี่คือหลักการพื้นฐานของ LuaJIT FFI: ลิงก์
    • คิดว่านี่คือเหตุผลที่ FFI ของ LuaJIT เร็วมาก
  • ข้อมูลเกี่ยวกับไลบรารีที่ใช้ JVMCI เพื่อสร้างโค้ด arm64/amd64 แบบทันทีและเรียกใช้ไลบรารีเนทีฟโดยไม่ต้องใช้ JNI: ลิงก์

  • ความเห็นที่ว่า "เขียน Ruby ให้มากที่สุดเท่าที่ทำได้ โดยเฉพาะเพราะ YJIT ปรับแต่งโค้ด Ruby ได้ แต่ทำแบบนั้นกับโค้ด C ไม่ได้"

    • สงสัยว่า Ruby ไม่ใช่ภาษาที่ค่อนข้างช้าอยู่แล้วหรือ
    • ถ้าจะลงไปที่เนทีฟ ก็อยากให้ทำงานให้ได้มากที่สุดเท่าที่เป็นไปได้ในเนทีฟ
  • ใช้ Ruby มานานกว่า 10 ปี และการได้เห็นพัฒนาการล่าสุดนั้นน่าสนใจมาก

    • ตั้งตารอ
  • สงสัยว่าทำไมจึงต้องใช้ JIT คอมไพล์

    • คิดว่าถ้าเขียนเป็น C ได้ ก็น่าจะคอมไพล์ตอนโหลดได้ไม่ใช่หรือ
  • FFI - Foreign Function Interface หรือก็คือวิธีที่ Ruby ใช้เรียก C

  • คำถามว่านี่ไม่ใช่สิ่งที่ libffi ทำอยู่แล้วหรือ

  • ตอนนี้พอจะเข้าใจแล้วว่าทำไมถึงไม่ไปที่ tenderlovemaking.com