This particular example crossed this painful divide 1 million times. I found it interesting that despite this disadvantage, the Crystal implementation was still able to take the lead over my identical, naively written Ruby implementation (warts and all). As the author of this post points out, for trivial operations crossing the interface at high frequency, finely tuned Ruby will easily take the lead!
That said, I still believe there are times where having the ability to write and interface with a performant, precompiled language (that is somewhat familiar to the average Rubyist) in an ergonomic way that avoids the need to context switch can be beneficial. Sure, performance is unlikely to match a finely tuned (but arguably more difficult to maintain) C or Rust extension and ergonomics are unlikely to match an approach that sticks to pure Ruby, but it exposes a new middle ground, which at times, may just hit the right spot!
I'd imagine realistic examples of where this type of library could be useful might include:
- Providing an easy way to expose and use high-quality Crystal shards from within your Ruby program.
- Allowing you to easily write performant CPU or memory-intensive procedures for which reusable native libraries do not exist, and where the majority of the overall execution time can be spent within Crystal.
- As a way to glue several different smaller Crystal shared objects together into a single application using Ruby glue code, allowing you to avoid some of the high compile times you might typically see with a large monolithic binary.
I would definitely not suggest this library has any business:
- Blindly replacing swaths of Ruby methods, without any tangible performance metrics to back this decision.
- Replacing code that is already highly performant in pure Ruby (whether that's code that lends itself well to being JIT'd, is backed by an existing native library etc.)
Funnily enough, if you take a look at the commit history of the project, you'll notice that last week I actually replaced the referenced example with one that better demonstrates a performance difference (even compared against YJIT) and crosses the FFI divide only once. This came as a result of having to introduce a Reactor to get the library to play nice in multi-threaded Ruby applications, which regrettably added even more overhead to the FFI interface and further hammers home the point that this library is not going to perform well in cases where you need to jump between Crystal and Ruby at high frequency.