Tiny JITs for a Faster FFI

FFI vs Separate Processes

  • One thread debates using Unix processes instead of FFI/native extensions: for many tasks (image conversion, zipping, analytics, simple AI jobs), spawning a CLI tool can be simpler and sufficiently fast.
  • Counterarguments: for hot-path work (e.g., DB access in Rails), shelling out is much slower than any in-process option and complicates deployment (extra binaries, build steps, containers). For performance-sensitive Ruby, spawning processes on the request path is seen as a non-starter.
  • Consensus: processes are fine for coarse-grained, asynchronous, or batch work; FFI/native extensions are still needed for tight loops and latency-sensitive operations.

Why “Write as Much Ruby as Possible”

  • Several comments explain that JITs can optimize managed-language code (Ruby) much more aggressively than opaque native calls.
  • FFI boundaries are unoptimizable: every call incurs marshaling and type checks, and the JIT can’t inline across them.
  • Examples from Java and JavaScript: code moved from C/C++ into the managed language once JITs became good enough; sometimes reverting back after JIT improvements.
  • The article’s approach—JIT-compiled FFI stubs—aims to make FFI faster so more loops can stay in Ruby instead of being pushed into bespoke C wrappers.

libffi vs Tiny JIT Stubs

  • Multiple comments clarify that libffi is largely table-driven and slow compared to specialized stubs.
  • libffi requires building descriptors and argument arrays, then a generic implementation walks and marshals them every call.
  • The “tiny JIT” approach in the article generates per-function machine code that:
    • Directly unboxes Ruby types to C types and vice-versa.
    • Avoids intermediate arrays and descriptor walking.
    • Can elide type checks when the Ruby side guarantees stable types.
  • Some initial confusion about libffi “JIT-ing” is corrected: only its closure trampolines are dynamically generated, and even those are minimal.

Ruby Performance, JIT, and C

  • There’s ongoing debate over how “fast” Ruby now is. With YJIT, Ruby can approach or beat some dynamic languages (often Python) on many workloads, though still far from C.
  • Several commenters stress that “Ruby beats C” claims are context-specific (e.g., Ruby+JIT vs Ruby+C-extension with FFI overhead) and often microbenchmark-driven.
  • Others emphasize that for most Rubyists the key win is: fewer C extensions, simpler builds, better portability across CRuby/JRuby/TruffleRuby, and improved hot-path performance, not surpassing C outright.