Tiny JITs เพื่อเพิ่มความเร็ว FFI
(railsatscale.com)- ใน 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ในเมธอด RubyB.strlen - เรียก C extension ที่สร้างด้วย Ruby Gem
strlen - เรียก
str.bytesizeโดยตรง
- เรียก
- ผลลัพธ์บน Ruby 3.5.0dev สภาพแวดล้อม ARM64:
ruby-direct: 39.879M i/s, 25.08 ns/istrlen-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ด้วยอินเทอร์เฟซที่คล้าย FFImodule Cextend FJITattach_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/istrlen-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 ความคิดเห็น
ความคิดเห็นจาก 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
โค้ด 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
ต้องขอบคุณ Rails At Scale และบทความต่าง ๆ ของ byroot ตอนนี้เป็นช่วงเวลาที่ดีมากที่จะได้อ่านบทความที่เจาะลึกโครงสร้างภายในและประสิทธิภาพของ Ruby และเมื่อรวมถึงการปรับปรุง Ruby กับ Rails ช่วงหลัง ๆ แล้ว ก็ถือว่าเป็นช่วงเวลาที่ค่อนข้างดีสำหรับการเป็นนักพัฒนา Ruby ด้วย
มันยังคงเป็นที่นิยมสำหรับแอปพลิเคชันบางประเภท แต่ดูเหมือนยุครุ่งเรืองจะผ่านมานานแล้ว และแม้การปรับปรุงล่าสุดจะดี แต่ก็ไม่แน่ใจว่าในปี 2025 JIT ยังน่าสนใจในเชิงเทคนิคขนาดนั้นหรือไม่
แนวทางที่ว่า “แทนที่จะเรียกไลบรารี third-party เรา JIT โค้ดที่จำเป็นสำหรับการเรียกฟังก์ชันภายนอกไปเลยไม่ได้หรือ?” ดูใกล้เคียงกับรากฐานของ LuaJIT FFI: https://luajit.org/ext_ffi.html
นั่นคงเป็นเหตุผลที่ FFI ของ LuaJIT เร็วมาก
ผมยังไม่ค่อยเข้าใจคำพูดที่ว่า “ให้เขียนด้วย Ruby ให้มากที่สุดเท่าที่ทำได้ โดยเฉพาะ YJIT สามารถปรับแต่งโค้ด Ruby ได้ แต่ปรับแต่งโค้ด C ไม่ได้”
Ruby เป็นภาษาที่ค่อนข้างช้าไม่ใช่หรือ? ถ้าจะลงไปเป็นเนทีฟ ก็น่าจะอยากให้ส่วนต่าง ๆ มากที่สุดเท่าที่ทำได้เป็น โค้ดเนทีฟ
ในรีลีสใหญ่หนึ่ง พบว่าโค้ด Java ที่จัดการพฤติกรรมขององค์ประกอบ UI บางอย่างเป็นคอขวด จึงถูกเขียนใหม่เป็น C ในรีลีสใหญ่ถัดมา
หลังจากนั้นเมื่อ JIT มีประโยชน์ขึ้นมาจริง ๆ overhead ของ FFI ก็ใหญ่กว่าความต่างระหว่างโค้ด C ที่จูนด้วยมือกับโค้ดที่ JIT สร้างออกมา และในรีลีสใหญ่ถัดไปจึงย้อนกลับไปเป็น อิมพลีเมนต์ Java ล้วน อีกครั้ง
ตามมาตรฐานของภาษายุคนั้น FFI ของ Java ถือว่าเร็วพอสมควร แต่หลังจากนั้นอีกไม่กี่รีลีสก็เปลี่ยนไปใช้วิธีที่ดีกว่า และตอนนั้นผมไม่ได้ตามโค้ด UI ของ Java มากนักแล้วจึงไม่ได้ติดตามต่อ ขณะเดียวกันก็มีการจัดระเบียบอินเทอร์เฟซระหว่างโค้ดเฉพาะแพลตฟอร์มกับโค้ด UI Java ทั่วไปอยู่ด้วย เลยไม่แน่ใจว่าสุดท้ายเป็นอย่างไร
งานแบบนี้ต้องคอยจับตา ผลแบบกระดานหก อยู่เสมอ ต้องชั่งใจว่ารออีกไม่กี่ milestone เพื่อลดงานจูนด้วยมือจะดีกว่าไหม หรือด้วยเหตุผลทางการเมือง·เทคนิคจำเป็นต้องทำตอนนี้ทันที
สำหรับงานที่รันครั้งเดียวแล้วจบอาจไม่มีประสิทธิภาพ แต่ใน workload เดสก์ท็อป·เซิร์ฟเวอร์ที่มีอายุยาว จะมีผลตอบแทนในมุมมองของแอปพลิเคชันโดยรวม
ตัวอย่างเช่น JIT ของ Dalvik ค่อนข้างอ่อน ทำให้การเรียกฟังก์ชันคณิตศาสตร์เป็น C เร็วกว่า แต่หลังจาก ART ก็ไม่จำเป็นต้องทำแบบนั้นอีกแล้ว และ JIT สามารถเอาชนะต้นทุนการเรียก C ได้
https://developer.android.com/reference/android/util/FloatMa...
แนวทางนี้บางครั้งเรียกว่า “self-hosting” และเบราว์เซอร์ก็ใช้กันมาก เช่น ย้ายส่วนที่เดิมน่าจะเขียนด้วย C/C++ ไปเป็น JavaScript ที่มีสิทธิ์พิเศษ ส่วนค่อนข้างใหญ่ของ standard library นั้นน่าประหลาดใจว่าไม่ใช่โค้ดเนทีฟ
แม้แต่ภาษาอย่าง C# ที่เร็วกว่ามากและมีต้นทุน interop เกือบเป็น 0 ก็ยังมีต้นทุนการเรียกอยู่ และบางครั้งยังต้องจ่ายต้นทุนการเปลี่ยน flag สถานะของ VM หรือการสลับโหมด garbage collection ด้วย
ถ้า Ruby YJIT เริ่มกลายเป็นปัจจัยที่วัดผลได้ กฎข้างต้นก็จะยิ่งสำคัญขึ้นเรื่อย ๆ
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
จากนั้นใน Ruby ก็รันโปรแกรม C นั้นผ่านเทอร์มินัลพร้อม flag หรือข้อมูล เท่านี้ Ruby ก็สามารถรันโค้ด C ได้
ไม่เข้าใจว่าทำไมต้อง คอมไพล์แบบ JIT ด้วย ถ้าเขียนเป็น C ได้ ก็แค่คอมไพล์ตอนโหลดไม่ได้หรือ?
สิ่งนี้เพิ่ม 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 จะสร้างอ็อบเจกต์ 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
จุดเด่นของบทความนี้คือ จากข้อมูลที่ผู้ใช้ส่งให้ตอนเรียก
attach_functionโค้ด unboxing ชนิดจะถูกแคชไว้ในโค้ดเครื่องที่สร้างขึ้นโดยพฤตินัยtramp.cที่ลิงก์ไว้ในคอมเมนต์ข้างเคียงมีไว้สำหรับ “FFI แบบย้อนกลับ” คือการเปิดเผยงานของผู้ใช้แบบไดนามิกเป็นฟังก์ชันพอยน์เตอร์ และ JIT ตรงนั้นอยู่ในระดับคำสั่งทั้งหมด 3 คำสั่งเพื่อเรียกโค้ดที่คอมไพล์ไว้ล่วงหน้าhttps://github.com/libffi/libffi/blob/master/src/tramp.c