Tiny JITs for a Faster FFI
45 comments
·February 12, 2025chris12321
jupp0r
Is it? To me it seems like Ruby is declining [1]. It's still popular for a specific niche of applications, but to me it seems like it's well past its days of glory. Recent improvements are nice, but is a JIT really that exciting technologically in 2025?
chris12321
Ruby will probably never again be the most popular language in the world, and it doesn't need to be for the people who enjoy it to be excited about the recent improvements in performance, documentation, tooling, ecosystem, and community.
adamtaylor_13
Rails is experiencing something of a renaissance in recent years. It’s easily one of the most pleasant programming experiences I’ve had in years.
All my new projects will be Rails. (What about projects that don’t lend themselves to Rails? I don’t take on such projects ;)
cship2
Hmm I thought Crystal was suppose to be faster Ruby? No?
obiefernandez
Stable mature technology trumps glory.
haberman
> Rather than calling out to a 3rd party library, could we just JIT the code required to call the external function?
I am pretty sure this is the basis of the LuaJIT FFI: https://luajit.org/ext_ffi.html
I think LuaJIT's FFI is very fast for this reason.
internetter
"write as much Ruby as possible, especially because YJIT can optimize Ruby code but not C code"
I feel like I'm not getting something. Isn't ruby a pretty slow language? If I was dipping into native I'd want to do as much in native as possible.
hinkley
There was a little drama that played out as Java was getting a proper JIT.
In one major release, there was a bunch of Java code responsible for handling some UI element activities. It was found to be a bottleneck, and rewritten in C code for the next major release.
Then the JIT became properly useful, and the FFI overhead was more than the difference between the hand-tuned C code and what the JIT would spit out on its own. So in the next major release, they rolled back to the all-Java implementation.
Java had a fairly reasonably fast FFI for that generation of programming language, but they swapped for a better one a few releases after that. And by then I wasn't doing a lot of Java UI code so I had stopped paying attention. But around the same time they were also making a cleaner interface between the platform-specific and the general Java code for UI, so I'm not entirely sure how that played out.
But that's exactly the sort of see-sawing you need to at least keep an eye out for when doing this sort of work. Would you be better off waiting a couple milestones and saving yourself a bunch of hand-tuning work, or do you need it right now for political or technical reasons?
kazinator
If FFI calls are slow (even slower than Ruby -> Ruby calls) then informs the way you use native code. You look for workflows whereby frequent calls to a FFI function are avoided: e.g. large number of calls in some inner loop. Suppose such a situation cannot be avoided. Then you may have no recourse than to move that loop out of Ruby into C: create a custom FFI for that use case which you can call once and have it execute the loop, calling many times the function you really wanted to call.
If the FFI call can be made faster, maybe you can keep the loop in Ruby.
Of course that is attractive to people writing an application in Ruby.
That's how I interpret keeping as much code Ruby as possible.
Nobody in their right mind wants to add additional application-specific jigs written in C just to use some C piece.
Once you start doing that, why even have FFI; you can just create a module.
One attractive point about FFI is that you can take some C library and use it in a higher level language without writing a line of C.
pjmlp
That is where a JIT enters the picture, ideally a JIT can re-optimize to an ideal state.
While this is suboptimal when doing one shot execution, when an application is long lived, mostly desktop or server workloads, this work pays off versus the overall application.
For example, Dalvik had a pretty lame JIT, thus it was faster calling into C for math functions, eventually with ART this was no longer needed, JIT could outperform the cost of calling into C.
https://developer.android.com/reference/android/util/FloatMa...
hahahacorn
There's a phenomenal breakdown by JPCamara (https://jpcamara.com/2024/12/01/speeding-up-ruby.html) on why the Ruby#each method was rewritten in Ruby (https://bugs.ruby-lang.org/issues/20182). And bonus content from tender love: https://railsatscale.com/2023-08-29-ruby-outperforms-c/.
TL;dr - JIT rules.
doppp
It's been fast for a while now.
Thaxll
Even a 50% or 2x speed improvment still make it a pretty slow language. It's in the Python range.
CyberDildonics
What is fast here? Ruby has usually been about 1/150th the speed of C.
kenhwang
If the code JITs well, Ruby performs somewhere between Go and NodeJS. Without the JIT, it's similar to Lua.
kevingadd
When dealing with a managed language that has a JIT or AOT compiler it's often ideal to write lots of stuff in the managed language, because that enables inlining and other optimizations that aren't possible when calling into C.
This is sometimes referred to as "self-hosting", and browsers do it a lot by moving things into privileged JavaScript that might normally have been written in C/C++. Surprisingly large amounts of the standard library end up being not written in native code.
kenhwang
Ruby has realized this as well. When running in YJIT mode, some standard library methods switch to using a pure ruby implementation instead of the C implementation because the YJIT-optimized-ruby is better performing.
internetter
Oh, I am indeed surprised! I guess I always assumed that most of the JavaScript standard library was written in C++
achierius
Well, most all of the compiler, runtime, allocator, garbage collector, object model, etc, are indeed written in C++ And so are many special operations (eg crypto functions, array sorts, walking the stack)
But specifically with regards to library functions, like the other commentator said, losing out on in lining sucks, and crossing between JS and Native code can be pretty expensive, so even with things like sorting an array it can be better to do it in js to avoid the overhead... Eg esp in cases where you can provide a callback as your comparator, which is js, and thus you have to cross back into js for every element
So it's a balancing game, and the various engines have gone back and forth on which functions are implemented in which language over time
neonsunset
FFI presents an opaque, unoptimizable boundary of code. Having chatty code like this is going to cost a lot. To the point where this is even a factor in much faster languages with zero-cost-ish interop like C# - you still have to make a call, sometimes paying the cost of modifying state flags for VM (GC transition).
If Ruby YJIT is starting to become a measurable factor (after all, it was slower than other, purely interpreted, languages until recently), then the same rule as above will become more relevant.
nialv7
isn't this exactly what libffi does?
kazinator
[delayed]
tenderlove
libffi can't know how to unwrap Ruby types (since it doesn't know what Ruby is). The advantage presented in this post is that the code for type unboxing is basically "cached" in the generated machine code based on the information the user passes when calling `attach_function`.
dzaima
libffi doesn't JIT for FFI calls; and it still requires you to lay out argument values yourself, i.e. for a string argument you'd still need to write code that converts a Ruby string to a C string. And libffi is rather slow.
(the tramp.c linked in a sibling comment is for "reverse-FFI", i.e. exposing some dynamic custom operation as a function pointer; and its JITting there amounts to a total of 3 instructions to call into precompiled code)
almostgotcaught
you know i thought i knew what libffi was doing (i thought it was playing tricks with GOT or something like that) but i think you're right
poisonta
I can sense why it didn’t go to tenderlovemaking.com
tenderlove
I think tenderworks wrote this post.
shortrounddev2
Does ruby have its equivalent to typescript, with type annotations? The language sounds interesting but I tend not to give dynamically typed languages the time of day
dragonwriter
> Does ruby have its equivalent to typescript, with type annotations?
Ruby has a first party external type definition format (RBS) as well as third-party typecheckers that check ruby against RBS definitions.
There is probably more use of the older, all third-party typing solution (Sorbet) though.
null
teaearlgraycold
This is the main thing keeping me from going back to Ruby. I don’t want to go back to the stone age where there’s no or poor static analysis
kevingadd
There's https://sorbet.org/ but it's not clear whether it has much adoption.
zem
I continue to think it was a big mistake not to add syntactic support for type annotations into the base language. python did this right; annotations are not enforced by the interpreter, but are accessible both by external tools as part of the AST and bytecode, and by the running program via introspection, so tools and libraries can do all sorts of interesting things with them.
having to add annotations in a separate header file is simply too high friction to get widespread adoption.
Lammy
IMHO (and I don't expect most people to agree but please be tolerant of my opinion!) annotations are annoying busywork that clutter my code and exist just to make people feel smart for “““doing correctness”””. The only check I find useful is nil or not-nil, and any halfway-well-designed interface should make it impossible for some unexpected object type to end up in the wrong place anyway. For anything less than halfway-well-defined, you have bigger issues than a lack of type annotation.
edit: I am quite fond of `case ::Ractor::receive; when SomeClass then …; when SomeOtherClass then …; end` as the main pattern for my Ractors though :)
pestatije
FFI - Foreign Function Interface, or how to call C from Ruby
tonetegeatinst
The totally safe and sane approach is to write C code that gets passed data via the command line during execution, then vomits results to the command line or just into a memory page.
Then just execute the c program with your flags or data in the terminal using ruby and viola, ruby can run C code.
grandempire
This. I think many people do not understand Unix processes and don’t realizing how rare it is to need bindings, ffi, and many libraries.
How many programs have an https client in them because they didn’t know they could use curl?
Between Rails At Scale and byroot's blogs, it's currently a fantastic time to be interested in in-depth discussions around Ruby internals and performance! And with all the recent improvements in Ruby and Rails, it's a great time to be a Rubyist in general!