2 คะแนน โดย GN⁺ 2025-02-14 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ใน Ruby เมื่อเรียกใช้ฟังก์ชันเนทีฟ FFI ใช้งานสะดวก แต่ในเบนช์มาร์ก strlen ค่าโอเวอร์เฮดในการเรียกสูงกว่า C extension จนกลายเป็นคอขวดด้านประสิทธิภาพ
  • ในเบนช์มาร์กอ้างอิง การเรียก String#bytesize โดยตรงทำได้ 39.879M i/s, C extension ทำได้ 30.661M i/s, การเรียกอ้อมผ่าน Ruby ทำได้ 28.697M i/s และ FFI ช้าที่สุดที่ 15.682M i/s
  • แนวคิดหลักคือใช้ชื่อฟังก์ชัน ประเภทอาร์กิวเมนต์ และประเภทค่าที่ส่งกลับ ซึ่งรู้อยู่แล้วตอน attach_function เพื่อสร้าง machine code สำหรับเรียกฟังก์ชันภายนอก ณ runtime
  • proof of concept FJIT บน Ruby 3.5.0dev สภาพแวดล้อม ARM64 เพิ่มความเร็วการเรียก strlen ได้ถึง 32.508M i/s เร็วกว่า FFI มากกว่า 2 เท่า และเร็วกว่า C extension เล็กน้อย
  • ปัจจุบันยังมีข้อจำกัด เช่น ARM64, อาร์กิวเมนต์เดียวและค่าที่ส่งกลับค่าเดียว, ประเภทข้อมูลที่รองรับจำกัด, ต้องใช้ --rjit --rjit-disable, และพึ่งพา commit เฉพาะของ Ruby head จึงต้องขยายการติดตั้งใช้งานก่อนนำไปใช้จริง

จุดที่ FFI ใน Ruby ช้าลง

  • แนวทางพื้นฐานคือเขียนโค้ด Ruby ให้มากที่สุด และเรียกโค้ดเนทีฟเฉพาะเมื่อจำเป็นจริง ๆ
  • YJIT สามารถปรับแต่งโค้ด Ruby ได้ แต่ไม่สามารถปรับแต่ง โค้ด C ได้
  • หากต้องใช้ไลบรารีเนทีฟ วิธีที่เหมาะคือวาง wrapper แบบ C extension บาง ๆ รอบฟังก์ชันจริง แล้วจัดการงานส่วนใหญ่ใน Ruby
  • API แบบเรียบง่ายเช่นนี้เข้ากับ FFI ได้ดี แต่ FFI เดิมยังทำประสิทธิภาพได้ไม่เท่า C extension

เบนช์มาร์กอ้างอิง strlen

  • รายการที่เปรียบเทียบมี 4 แบบ
    • เรียก strlen ของ C ผ่าน FFI
    • เรียกอ้อม x.bytesize ในเมธอด Ruby B.strlen
    • เรียก C extension ที่สร้างด้วย Ruby Gem strlen
    • เรียก str.bytesize โดยตรง
  • ผลลัพธ์บน Ruby 3.5.0dev สภาพแวดล้อม ARM64:
    • ruby-direct: 39.879M i/s, 25.08 ns/i
    • strlen-cext: 30.661M i/s, 32.61 ns/i, ช้ากว่าการเรียกโดยตรง 1.30 เท่า
    • strlen-ruby: 28.697M i/s, 34.85 ns/i, ช้ากว่าการเรียกโดยตรง 1.39 เท่า
    • strlen-ffi: 15.682M i/s, 63.77 ns/i, ช้ากว่าการเรียกโดยตรง 2.54 เท่า
  • การเรียก String#bytesize โดยตรงเร็วที่สุด และ การเรียกอ้อม เพิ่มเติมทำให้มีโอเวอร์เฮดมากขึ้น
  • ความต่างระหว่าง ruby-direct กับ strlen-ruby แสดงให้เห็นต้นทุนการ push/pop stack frame และการกำจัดโอเวอร์เฮดลักษณะนี้เป็นส่วนที่ JIT compiler อย่าง YJIT จัดการได้ดี
  • ช่องว่างระหว่าง strlen-cext กับ strlen-ffi เผยให้เห็นว่าการเรียกฟังก์ชันเนทีฟผ่าน FFI มีต้นทุนเพิ่มเติมสูง

วิธีแทนที่การเรียก FFI ด้วย JIT

  • ณ ตอนเรียก attach_function :strlen, [:string], :int มีข้อมูลที่จำเป็นอยู่แล้ว
    • ชื่อฟังก์ชันที่จะเรียก: strlen
    • ประเภทอาร์กิวเมนต์: string
    • ประเภทค่าที่ส่งกลับ: int
  • เมื่อใช้ข้อมูลนี้ จะสามารถสร้าง machine code ที่แกะค่า Ruby ออกเป็นประเภทเนทีฟ เรียกฟังก์ชันภายนอก แล้วห่อค่าที่ส่งกลับกลับเป็นออบเจ็กต์ Ruby ได้
  • องค์ประกอบที่จำเป็นมี 3 อย่าง
    • AArch64 gem: สร้าง machine code สำหรับ ARM64
    • Fisk gem: สร้าง machine code สำหรับ x86_64
    • JITBuffer gem: จัดสรรหน่วยความจำที่รันได้
  • การสร้าง machine code อย่างเดียวยังไม่พอ Ruby ต้องสามารถ jump ไปยัง machine code นั้นได้ จึงจะข้าม โอเวอร์เฮดของ FFI ได้

เส้นทางที่ใช้ RJIT

  • RJIT คือ JIT compiler สำหรับ Ruby ที่เขียนด้วย Ruby และแจกมาพร้อมกับ Ruby
  • โครงสร้างภายในคล้ายกับ YJIT แต่ไม่ได้มุ่งใช้ในโปรดักชัน จึงไม่เป็นที่รู้จักแพร่หลายเท่า YJIT
  • Kokubun ยื่น feature request เพื่อแยก RJIT ออกมาเป็น Gem
  • ข้อเสนอนี้มอบฐาน 2 อย่างเพื่อให้สร้าง JIT compiler ของ Ruby จาก third party ได้ง่ายขึ้น
    • แยก RJIT ออกเป็น Gem
    • สร้าง type ภายในของ Ruby เป็นโครงสร้างข้อมูล Ruby เพื่อให้ JIT จาก third party ได้ข้อมูลที่จำเป็นสำหรับการ wrap และ unwrap ประเภทข้อมูลของ Ruby
  • การเปลี่ยนแปลงอีกอย่างคือ หากมี JIT entry function pointer ก็ให้รันเสมอ
    • เมื่อ JIT จาก third party ลงทะเบียน machine code แล้ว Ruby จะ jump ไปยังโค้ดนั้นโดยอัตโนมัติ
  • หากมีสององค์ประกอบนี้ ก็สามารถสร้าง JIT compiler ขนาดเล็ก จุดประสงค์เดียว ที่ทำหน้าที่เป็นอินเทอร์เฟซ FFI ได้

Proof of concept FJIT

  • Proof of concept FJIT ย่อมาจาก “FFI JIT” และสร้าง machine code สำหรับเรียกฟังก์ชันภายนอก ณ runtime
  • ตัวอย่างแนบ strlen ด้วยอินเทอร์เฟซที่คล้าย FFI
    • module C
    • extend FJIT
    • attach_function :strlen, [:string], :int
  • เมื่อ attach_function ถูกเรียก FJIT จะสร้าง machine code ที่แกะสตริง Ruby, เรียก strlen ของ C และส่งคืนความยาวสตริงเป็นออบเจ็กต์ Ruby

ผลเบนช์มาร์ก FJIT

  • ผลลัพธ์บน Ruby 3.5.0dev, +RJIT +PRISM, สภาพแวดล้อม ARM64:
    • ruby-direct: 41.907M i/s, 23.86 ns/i
    • strlen-fjit: 32.508M i/s, 30.76 ns/i, ช้ากว่าการเรียกโดยตรง 1.29 เท่า
    • strlen-cext: 29.778M i/s, 33.58 ns/i, ช้ากว่าการเรียกโดยตรง 1.41 เท่า
    • strlen-ruby: 28.851M i/s, 34.66 ns/i, ช้ากว่าการเรียกโดยตรง 1.45 เท่า
    • strlen-ffi: 15.629M i/s, 63.98 ns/i, ช้ากว่าการเรียกโดยตรง 2.68 เท่า
  • การเรียก String#bytesize โดยตรงยังคงเร็วที่สุด
  • machine code ที่ FJIT สร้างเร็วเป็นอันดับสอง และให้ผลดีกว่า C extension ของ strlen เล็กน้อย
  • FJIT เร็วกว่า 2 เท่าขึ้นไป เมื่อเทียบกับการเรียก FFI และยังเร็วกว่าการเรียกอ้อมผ่าน Ruby ด้วย
  • ผลลัพธ์นี้ชี้ว่าอาจรักษาแนวทาง “เขียน Ruby ให้มากที่สุด” ไว้ได้ พร้อมกับได้ความเร็วเท่ากับหรือดีกว่า C extension

ข้อจำกัดที่เหลือก่อนนำไปใช้จริง

  • JIT compiler แบบ proof of concept ปัจจุบันจำกัดอยู่ที่ แพลตฟอร์ม ARM64
    • หากจะขยายเป็นการใช้งานจริง ต้องเพิ่ม backend สำหรับ x86_64
  • ยังไม่ได้รองรับประเภทอาร์กิวเมนต์และประเภทค่าที่ส่งกลับทั้งหมด
    • คาดว่าการรองรับอาร์กิวเมนต์ทุกประเภททำได้ และปริมาณงานไม่น่ามากเกินไป
  • ตอนนี้รองรับเฉพาะฟังก์ชันที่รับอาร์กิวเมนต์เดียวและส่งกลับค่าเดียว
  • ตอนนี้ต้องรัน Ruby ด้วย flag --rjit --rjit-disable
    • คาดว่าเมื่อฟีเจอร์ของ Kokubun เข้ามา เงื่อนไขนี้จะไม่จำเป็นอีกต่อไป
  • proof of concept รันได้เฉพาะบน Ruby head ณ เวลานั้น
  • ตามข้อมูลอัปเดต RJIT ถูกลบออกจาก Ruby head แล้ว ดังนั้นหากต้องการรันสคริปต์ ต้อง checkout Ruby ไปที่ commit f32d5071b7b01f258eb45cf533496d82d5c0f6a1

1 ความคิดเห็น

 
GN⁺ 2025-02-14
ความคิดเห็นจาก Hacker News
  • ตัวแก้ปัญหา constraint ใน Java ชื่อ Timefold ต้องทำให้เรียกฟังก์ชันที่นิยามไว้ใน CPython ได้ จึงได้ข้องเกี่ยวกับ FFI อยู่มาก แต่ปัญหาด้านประสิทธิภาพส่วนใหญ่เกิดขึ้นตอนสลับไปมาระหว่างภาษาโฮสต์กับภาษาภายนอกผ่าน พร็อกซี
    ถ้าเรียก FFI โดยตรงผ่าน JNI หรืออินเทอร์เฟซภายนอกตัวใหม่ ก็เร็วพอ ๆ กับการเรียกเมธอด Java โดยตรง แต่ garbage collector ของ CPython กับ Java เข้ากันได้ไม่ดีนัก จึงต้องใช้มนตร์ดำในการซิงโครไนซ์
    ในทางกลับกัน พร็อกซีอย่าง JPype หรือ GraalPy จำเป็นต้องแปลงพารามิเตอร์และค่าที่คืนกลับมา และอาจทำให้เกิดการเรียก FFI ย้อนทิศทางเพิ่มอีกด้วย ถ้าส่งอ็อบเจ็กต์ CPython ให้ Java, Java จะสร้างพร็อกซีของอ็อบเจ็กต์นั้นขึ้นมา และเมื่อส่งพร็อกซีนั้นกลับไปให้ CPython อีกครั้ง มันจะไม่แกะออก แต่กลับสร้างพร็อกซีของพร็อกซีขึ้นมาแทน
    ผลคือพร็อกซีของ JPype ช้ากว่าการเรียก CPython โดยตรงผ่าน FFI 1402% และพร็อกซีของ GraalPy ช้ากว่า 453%
    สุดท้ายจึงแปล CPython bytecode เป็น Java bytecode และสร้างโครงสร้างข้อมูลฝั่ง Java ที่สอดคล้องกับคลาสของ CPython ที่ถูกใช้ ทำให้เร็วขึ้น 100 เท่าเมื่อเทียบกับการใช้พร็อกซี อนึ่ง CPython bytecode นั้นไม่เสถียรอย่างมาก เอกสารก็มีน้อย และคุณลักษณะของ VM ก็ยุ่งยาก ทำให้แมปตรง ๆ ไปยัง bytecode อื่นได้ยาก ดังนั้นจะดีกว่าถ้าไม่พยายามแปลหรืออ่านมัน
    รายละเอียดเพิ่มเติมสรุปไว้ในบทความนี้: https://timefold.ai/blog/java-vs-python-speed

    • พูดแบบไม่มีประสบการณ์ตรง FFI จาก Python และ Java ไปยัง C ตามลำดับดูจะดีกว่ามาก และผมคิดว่าการเชื่อมสองฝั่งด้วย C bridge เล็ก ๆ อาจกลายเป็นวิธีทั่วไปได้
    • cgo is not Go ก็น่าอ่านประกอบเช่นกัน
      โค้ด Go กับโค้ด C ต้องตกลงกันว่าจะใช้ทรัพยากรร่วมกันอย่างไร เช่น address space, signal handler, สล็อต TLS ของเธรด แต่ในทางปฏิบัติมันใกล้เคียงกับการที่ Go ต้องหลบเลี่ยงสมมติฐานของโค้ด C มากกว่า โค้ด C อาจสมมติว่ามันรันบนเธรดเดียวเท่านั้น หรืออาจไม่ได้เตรียมพร้อมเลยสำหรับสภาพแวดล้อมแบบมัลติเธรด
      C ไม่รู้จัก calling convention ของ Go หรือสแตกที่ขยายได้ ดังนั้นเมื่อต้องเรียกโค้ด C จึงต้องบันทึกรายละเอียดของสแตก goroutine สลับไปใช้สแตก C แล้วจึงรันโค้ด C ที่ไม่รู้ว่าตัวเองถูกเรียกมาอย่างไร และไม่รู้จัก Go runtime ทั้งระบบ
      ไม่ว่าจะทำ binding หรือห่อหุ้มโค้ด C ด้วย Python, Java ที่ใช้ JNI, ภาษาที่ใช้ libffi หรือ Go ที่ใช้ cgo ท้ายที่สุดก็เท่ากับต้องใช้ชีวิตอยู่ใน โลกของ C
      https://dave.cheney.net/2016/01/18/cgo-is-not-go / https://archive.vn/GZoMK
    • กรณีแบบนี้ทำให้อยากรู้ว่าวิธี สื่อสารระหว่างโปรเซส อย่างคิว ไฟล์ หรือ HTTP จะเหมาะสมในจุดไหน
  • ต้องขอบคุณ Rails At Scale และบทความต่าง ๆ ของ byroot ตอนนี้เป็นช่วงเวลาที่ดีมากที่จะได้อ่านบทความที่เจาะลึกโครงสร้างภายในและประสิทธิภาพของ Ruby และเมื่อรวมถึงการปรับปรุง Ruby กับ Rails ช่วงหลัง ๆ แล้ว ก็ถือว่าเป็นช่วงเวลาที่ค่อนข้างดีสำหรับการเป็นนักพัฒนา Ruby ด้วย

    • จริงหรือเปล่านะ Ruby ดูเหมือนอยู่ในขาลง https://www.tiobe.com/tiobe-index/ruby/
      มันยังคงเป็นที่นิยมสำหรับแอปพลิเคชันบางประเภท แต่ดูเหมือนยุครุ่งเรืองจะผ่านมานานแล้ว และแม้การปรับปรุงล่าสุดจะดี แต่ก็ไม่แน่ใจว่าในปี 2025 JIT ยังน่าสนใจในเชิงเทคนิคขนาดนั้นหรือไม่
  • แนวทางที่ว่า “แทนที่จะเรียกไลบรารี third-party เรา JIT โค้ดที่จำเป็นสำหรับการเรียกฟังก์ชันภายนอกไปเลยไม่ได้หรือ?” ดูใกล้เคียงกับรากฐานของ LuaJIT FFI: https://luajit.org/ext_ffi.html
    นั่นคงเป็นเหตุผลที่ FFI ของ LuaJIT เร็วมาก

  • ผมยังไม่ค่อยเข้าใจคำพูดที่ว่า “ให้เขียนด้วย Ruby ให้มากที่สุดเท่าที่ทำได้ โดยเฉพาะ YJIT สามารถปรับแต่งโค้ด Ruby ได้ แต่ปรับแต่งโค้ด C ไม่ได้”
    Ruby เป็นภาษาที่ค่อนข้างช้าไม่ใช่หรือ? ถ้าจะลงไปเป็นเนทีฟ ก็น่าจะอยากให้ส่วนต่าง ๆ มากที่สุดเท่าที่ทำได้เป็น โค้ดเนทีฟ

    • ตอนที่ Java กำลังเริ่มมี JIT ที่ใช้งานได้จริง ก็เคยมีดราม่าเล็ก ๆ แบบนี้อยู่
      ในรีลีสใหญ่หนึ่ง พบว่าโค้ด Java ที่จัดการพฤติกรรมขององค์ประกอบ UI บางอย่างเป็นคอขวด จึงถูกเขียนใหม่เป็น C ในรีลีสใหญ่ถัดมา
      หลังจากนั้นเมื่อ JIT มีประโยชน์ขึ้นมาจริง ๆ overhead ของ FFI ก็ใหญ่กว่าความต่างระหว่างโค้ด C ที่จูนด้วยมือกับโค้ดที่ JIT สร้างออกมา และในรีลีสใหญ่ถัดไปจึงย้อนกลับไปเป็น อิมพลีเมนต์ Java ล้วน อีกครั้ง
      ตามมาตรฐานของภาษายุคนั้น FFI ของ Java ถือว่าเร็วพอสมควร แต่หลังจากนั้นอีกไม่กี่รีลีสก็เปลี่ยนไปใช้วิธีที่ดีกว่า และตอนนั้นผมไม่ได้ตามโค้ด UI ของ Java มากนักแล้วจึงไม่ได้ติดตามต่อ ขณะเดียวกันก็มีการจัดระเบียบอินเทอร์เฟซระหว่างโค้ดเฉพาะแพลตฟอร์มกับโค้ด UI Java ทั่วไปอยู่ด้วย เลยไม่แน่ใจว่าสุดท้ายเป็นอย่างไร
      งานแบบนี้ต้องคอยจับตา ผลแบบกระดานหก อยู่เสมอ ต้องชั่งใจว่ารออีกไม่กี่ milestone เพื่อลดงานจูนด้วยมือจะดีกว่าไหม หรือด้วยเหตุผลทางการเมือง·เทคนิคจำเป็นต้องทำตอนนี้ทันที
    • ตรงจุดนี้เองที่ JIT เข้ามามีบทบาท ตามอุดมคติ JIT สามารถปรับแต่งโค้ดใหม่ให้ดีขึ้นได้
      สำหรับงานที่รันครั้งเดียวแล้วจบอาจไม่มีประสิทธิภาพ แต่ใน workload เดสก์ท็อป·เซิร์ฟเวอร์ที่มีอายุยาว จะมีผลตอบแทนในมุมมองของแอปพลิเคชันโดยรวม
      ตัวอย่างเช่น JIT ของ Dalvik ค่อนข้างอ่อน ทำให้การเรียกฟังก์ชันคณิตศาสตร์เป็น C เร็วกว่า แต่หลังจาก ART ก็ไม่จำเป็นต้องทำแบบนั้นอีกแล้ว และ JIT สามารถเอาชนะต้นทุนการเรียก C ได้
      https://developer.android.com/reference/android/util/FloatMa...
    • ใน ภาษาแบบ managed ที่มี JIT หรือคอมไพเลอร์ AOT หลายครั้งอุดมคติคือเขียนโค้ดจำนวนมากด้วยภาษานั้นเอง เพราะจะทำให้ทำ inlining และ optimization อื่น ๆ ที่เป็นไปไม่ได้เมื่อข้ามขอบเขตการเรียก C ได้
      แนวทางนี้บางครั้งเรียกว่า “self-hosting” และเบราว์เซอร์ก็ใช้กันมาก เช่น ย้ายส่วนที่เดิมน่าจะเขียนด้วย C/C++ ไปเป็น JavaScript ที่มีสิทธิ์พิเศษ ส่วนค่อนข้างใหญ่ของ standard library นั้นน่าประหลาดใจว่าไม่ใช่โค้ดเนทีฟ
    • FFI สร้าง ขอบเขตที่ทึบและปรับแต่งไม่ได้ ระหว่างโค้ด โค้ดที่ข้ามไปมาบ่อย ๆ แบบนี้มีต้นทุนสูง
      แม้แต่ภาษาอย่าง C# ที่เร็วกว่ามากและมีต้นทุน interop เกือบเป็น 0 ก็ยังมีต้นทุนการเรียกอยู่ และบางครั้งยังต้องจ่ายต้นทุนการเปลี่ยน flag สถานะของ VM หรือการสลับโหมด garbage collection ด้วย
      ถ้า Ruby YJIT เริ่มกลายเป็นปัจจัยที่วัดผลได้ กฎข้างต้นก็จะยิ่งสำคัญขึ้นเรื่อย ๆ
    • มีบทความที่ JPCamara อธิบายได้ยอดเยี่ยมว่าทำไมเมธอด each ของ Ruby จึงถูกเขียนใหม่ด้วย Ruby: https://jpcamara.com/2024/12/01/speeding-up-ruby.html / https://bugs.ruby-lang.org/issues/20182
      และยังมีบทความโบนัสของ tender love ด้วย: https://railsatscale.com/2023-08-29-ruby-outperforms-c/
      สรุปคือ JIT ชนะ นั่นเอง
  • FFI คือ Foreign Function Interface หรือวิธีที่ Ruby ใช้เรียก C

    • แนวทางที่ปลอดภัยและสมเหตุสมผลเต็มที่คือเขียนโค้ด C ที่รับข้อมูลผ่าน command line ตอนรัน แล้วพ่นผลลัพธ์ออกมาทาง command line หรือ memory page
      จากนั้นใน Ruby ก็รันโปรแกรม C นั้นผ่านเทอร์มินัลพร้อม flag หรือข้อมูล เท่านี้ Ruby ก็สามารถรันโค้ด C ได้
  • ไม่เข้าใจว่าทำไมต้อง คอมไพล์แบบ JIT ด้วย ถ้าเขียนเป็น C ได้ ก็แค่คอมไพล์ตอนโหลดไม่ได้หรือ?

    • ไม่แน่ใจว่าหมายถึงสามารถเขียนอะไรเป็น C ได้ ไลบรารี FFI ช่วยให้ Ruby bind และรันเมธอดของไลบรารีแบบไดนามิกได้โดยไม่ต้องเขียน native extension
      สิ่งนี้เพิ่ม productivity ได้มาก และทำให้แชร์โค้ดเดียวกันระหว่าง CRuby, JRuby, TruffleRuby ได้
      ถ้าสามารถรู้ binding ทั้งหมดแบบสแตติกตอนบูตได้ ก็อาจเขียน stub แล้วใส่ลงใน method table ได้ แต่ถึงอย่างไรก็เป็นสิ่งที่เกิดขึ้นตอน runtime จึงจัดเป็น JIT อยู่ดี และเพราะไม่สามารถปรับตัวตาม type ที่ไหลผ่านระบบได้ จึงจำเป็นต้องอนุรักษนิยมในค่าที่รับหรือการ optimization ซึ่งใกล้เคียงกับสิ่งที่ libffi ทำอยู่ในปัจจุบัน
      แนวทาง AOT ก็คือการเขียน native extension
  • นอกเรื่องเล็กน้อย แต่ปกติผมมักหลีกเลี่ยง gem ที่ใช้ FFI มากกว่าหลีกเลี่ยง FFI เอง เพราะ การคอมไพล์ มักยุ่งยากเกินไป จนหลายครั้งการข้ามขั้นกลางของ Rubygems/bundler แล้ว build เองโดยตรงง่ายกว่า

  • เกี่ยวข้องกันเล็กน้อย ไลบรารีนี้ใช้ JVMCI เพื่อสร้างโค้ด arm64/amd64 แบบ on-the-fly สำหรับเรียกไลบรารีเนทีฟโดยไม่ผ่าน JNI: https://github.com/apangin/nalim

  • นี่ไม่ใช่สิ่งที่ libffi ทำอยู่พอดีเหรอ?

    • libffi ช้า และเท่าที่ผมรู้มันไม่ได้ทำ JIT
      ใน libffi จะสร้างอ็อบเจกต์ descriptor สำหรับฟังก์ชัน และโครงสร้างข้อมูล runtime นี้จะแสดงชนิดของอาร์กิวเมนต์และค่าที่ส่งกลับ
      ตอนเรียก FFI ต้องส่ง descriptor พร้อมกับอาร์เรย์ของพอยน์เตอร์ไปยังค่าที่ต้องการส่งผ่าน ภายในก็น่าจะวนทั้งอาร์เรย์ค่าและ descriptor ไปด้วยกัน แล้ววางค่าลงบนสแตกตามชนิด จากนั้นเมื่อฟังก์ชันจบก็หยิบค่าตามชนิดที่ส่งกลับออกมา ขั้นตอนนี้มีโอกาสสูงที่จะมีการ branch ตามชนิดแทรกอยู่หลายจุด
      ต่อให้กลไกการเรียกของ libffi ถูก JIT ก็ตาม งานเตรียมอาร์เรย์อาร์กิวเมนต์ก็ยังช้าอยู่ดี มันไม่ตรงไปตรงมาเท่า FFI JIT ที่เข้าถึงอาร์กิวเมนต์โดยตรงโดยไม่ผ่านอาร์เรย์กลาง
      โค้ด FFI JIT รับค่าอาร์กิวเมนต์โดยตรง แปลงจากชนิด Ruby เป็นชนิด C ใส่ค่าแต่ละตัวลงในตำแหน่งที่ถูกต้องบนสแตกหรือรีจิสเตอร์ด้วยโค้ด inline แล้วเรียกฟังก์ชัน จากนั้นแปลงค่าที่ส่งกลับเป็นชนิด Ruby โดยพื้นฐานแล้วคล้ายกับการเขียนโค้ด extension ด้วยมือ
      ถ้ามี type inference ก็สามารถข้ามการตรวจชนิดในโค้ดแปลงค่าได้ เช่น ถ้ารับประกันได้ว่า arg1 เป็นสตริง Ruby ก็ใช้ฟังก์ชันแปลงเวอร์ชัน unsafe ที่เร็วกว่าได้
      ในกรณีแย่ที่สุด โค้ด JIT แค่ต้องสะท้อนชนิด Ruby ประมาณนั้น และไม่ต้องมีอาร์เรย์หรือรายการเกี่ยวกับอาร์กิวเมนต์ เพราะชนิด C ที่จะแปลงไปถูก hard-code อยู่ในโค้ดแล้ว จึงไม่จำเป็นต้องวนผ่านโครงสร้างข้อมูลที่อธิบายฝั่ง C ตอน runtime
    • libffi ไม่รู้ว่า Ruby คืออะไร จึงไม่มีทางรู้วิธี unbox ชนิดของ Ruby
      จุดเด่นของบทความนี้คือ จากข้อมูลที่ผู้ใช้ส่งให้ตอนเรียก attach_function โค้ด unboxing ชนิดจะถูกแคชไว้ในโค้ดเครื่องที่สร้างขึ้นโดยพฤตินัย
    • libffi ไม่ได้ JIT การเรียก FFI และยังต้องจัดวางค่าอาร์กิวเมนต์เองอยู่ดี เช่น ถ้าเป็นอาร์กิวเมนต์สตริง ก็ต้องเขียนโค้ดแยกเพื่อแปลงอ็อบเจกต์สตริง Ruby เป็นพอยน์เตอร์สตริง C และ libffi ก็ค่อนข้างช้า
      tramp.c ที่ลิงก์ไว้ในคอมเมนต์ข้างเคียงมีไว้สำหรับ “FFI แบบย้อนกลับ” คือการเปิดเผยงานของผู้ใช้แบบไดนามิกเป็นฟังก์ชันพอยน์เตอร์ และ JIT ตรงนั้นอยู่ในระดับคำสั่งทั้งหมด 3 คำสั่งเพื่อเรียกโค้ดที่คอมไพล์ไว้ล่วงหน้า
    • ผมนึกว่ารู้ว่า libffi ทำอะไรอยู่ คิดว่ามันเล่นทริกอะไรสักอย่างกับ GOT แต่ดูเหมือนคุณจะพูดถูก
      https://github.com/libffi/libffi/blob/master/src/tramp.c