May 31, 2018·5 min read·engineeringrubyjitruntime

Ruby 2.6 preview tests a JIT built around a C compiler

Ruby 2.6 preview 2 exposes a method JIT that generates C and invokes GCC or Clang at runtime. The preview tests the compilation infrastructure before promising final application performance.

Ruby's experimental JIT delegates native-code generation to a compiler that normally runs before an application starts. It writes generated C to disk and invokes GCC or Clang while Ruby is running. This makes an established optimizing compiler part of the execution environment.

The May 31 Ruby 2.6 preview 2 announcement describes the initial JIT implementation and its limits. The immediate purpose is to check platform compatibility and identify security risks before the final release. Very few optimizations are implemented, and the announcement explicitly says the preview is unsuitable for evaluating final JIT performance on large applications such as Rails.

The preview's MJIT implementation explains the engineering tradeoff. Common C compilers optimize generated code well, but compile more slowly than a typical JIT engine. MJIT therefore needs to manage the cost and timing of invoking them.

I appreciate Ruby's readable expressions and collection operations, but their execution cost still matters. This preview is interesting because it makes that cost an implementation problem with a visible compilation pipeline. It is too early to turn that design into a general speed claim.

Ruby instruction sequences become C

The implementation takes Ruby instruction sequences and generates C code. A C compiler produces position-independent native code, which Ruby loads for later execution. The source describes a JIT unit associated with an instruction sequence and a handle for its loaded object.

This gives the runtime an additional implementation of work that could otherwise be interpreted. When generated code is already available for an instruction sequence, the runtime can reuse it. Compiling once and executing repeatedly is central to making the external compiler's cost worthwhile.

The generated code is not an unconditional replacement for interpretation. The implementation comments describe returning to instruction-sequence interpretation when a required condition is not satisfied or an exception occurs. Speculative generated code can also be canceled.

That fallback is part of the execution model. Dynamic behavior can invalidate assumptions made by a compiled path. Retaining an interpreter gives the runtime a way to continue when that path no longer applies. A native-code result therefore participates in the Ruby runtime's rules instead of becoming an unrelated standalone program.

The architecture reuses the optimization and code-generation machinery of GCC or Clang. It also inherits the work involved in launching a compiler and handling its output. Both aspects follow from the same decision to use C as an intermediate representation.

A worker keeps compilation separate from execution

MJIT's infrastructure uses a worker thread to perform compilation alongside instruction-sequence execution. The worker prepares a precompiled header, selects a queued unit, translates its instructions to C and invokes the compiler. When the generated code is ready, it can load the result.

The queue can prioritize instruction sequences with larger call counts. The implementation's selection function searches for the unit with the greatest total number of calls. That policy directs compilation effort toward repeatedly executed work.

This is an important distinction from translating every method immediately. Native compilation has a cost even if the resulting code never executes enough times to repay it. A queue informed by call counts gives the runtime a way to concentrate the work.

The implementation also coordinates JIT work with garbage collection. Its hooks prevent compilation activity from conflicting with collection and wake workers afterward. A compiler operating alongside a dynamic runtime needs this synchronization because the objects describing executable code have their own lifetime.

Precompiled headers are another cost-control measure. The generated units use a prepared header instead of repeating all header processing for every compilation. This helps explain why the preview emphasizes infrastructure. Scheduling, generated inputs and runtime coordination must work before more optimizations can be evaluated.

Enabling the preview changes runtime prerequisites

The announcement requires Ruby to have been built with GCC or Clang, with the compiler also available at runtime. A deployment containing only the interpreter is therefore not sufficient for this experimental mode. The running environment needs the compiler used by the JIT pipeline.

The command-line option enables it explicitly.

ruby --jit --jit-verbose=1 program.rb

The verbosity option prints basic information about ongoing compilation. This can establish that JIT activity is occurring while examining the preview. A successful program result alone does not show whether a particular method reached generated execution.

The announcement also permits enabling JIT through RUBYOPT. That changes the same execution policy through process configuration. The requirement for a runtime compiler remains regardless of how the option is supplied.

Generated C and compiled objects add filesystem activity to the process's execution environment. The implementation describes temporary files and a compiler subprocess. The release's request to look for security risks is therefore concrete, involving new operations performed while executing Ruby code.

My preference is to understand that operational change before discussing speed. A runtime compiler dependency affects the environment used to run the program, even when the Ruby source stays unchanged. The preview provides a chance to examine those requirements before they become part of a final-release deployment choice.

Other preview changes have different maturity signals

The preview introduces an experimental RubyVM::AST API for parsing strings or files into syntax nodes. The announcement warns that the node structure has no compatibility guarantee. This is separate from the JIT but reflects the same need to distinguish an experimental interface from a stable application contract.

Endless ranges also appear in the preview. They express a range with no end and support examples such as selecting an array's remaining elements or iterating from an initial index. These language changes do not depend on whether JIT is enabled.

The release announcement reports improvements to Proc calls and calls through passed block parameters, with specific microbenchmark observations. Those interpreter and calling-path changes should not be attributed to MJIT merely because they ship in the same preview. The source describes them separately.

For MJIT itself, the publication asks for platform and security feedback and defers final performance judgments. Its present contribution is a working path that translates Ruby instruction sequences through C, manages compilation with a worker and loads native results back into the runtime. That is the implementation available to examine in preview 2.