Ruby 2.6 adds an experimental compiler alongside runtime improvements
Ruby 2.6 ships MJIT, Bundler and endless ranges. Its performance work spans native compilation, short-lived allocation and block calls, with different benefits and maturity guarantees.
Ruby 2.6's new JIT is one part of its performance work. The release also changes allocation for short-lived objects, improves calls through blocks and adds native coroutine implementations. These improvements operate at different points in execution, so a result attributed simply to Ruby 2.6 can conceal which mechanism helped.
The December 25 release announcement keeps the JIT explicitly experimental. It reports a 1.7-fold improvement over Ruby 2.5 on Optcarrot, a CPU-intensive benchmark, while warning that memory-intensive workloads such as Rails may not benefit. The release's performance claims are specific to their workloads.
The tagged NEWS file also records a substantial change to the distribution. Bundler 1.17.2 is included as a default gem, alongside RubyGems 3.0.1. Dependency management becomes part of the supplied Ruby environment without changing the fact that its tools have their own versions and compatibility rules.
I find Ruby's blocks and collection methods readable, especially for expressing small transformations. Runtime improvements are useful when they preserve those expressions while reducing their implementation cost. This release deserves a more precise account than treating the experimental compiler as the explanation for every improvement.
MJIT makes a C compiler a runtime dependency
MJIT generates C source, writes it to disk and invokes a C compiler to produce native code. Ruby then uses the generated implementation during program execution. This architecture reuses a conventional optimizing compiler instead of embedding all native-code generation inside the Ruby process.
The final release supports GCC, Clang and Microsoft Visual C++ for this path. The announcement says the compiler must be available at runtime. Enabling JIT therefore requires more than installing a Ruby executable with the option recognized.
The documented invocation is explicit.
ruby --jit --jit-verbose=1 program.rb
The first option enables compilation, and the verbosity setting reports additional JIT activity. RUBYOPT can also carry the option. Neither form makes the compiler's runtime availability optional.
The architecture has an execution cost before compiled methods can help. It generates input, launches another process and loads the result. A program must execute suitable code often enough for native execution to compensate for that preparation. The reported Optcarrot result is evidence for one workload with that overall tradeoff.
The release warning about Rails is consequently substantive. A CPU-intensive benchmark and an allocation-heavy application exercise different proportions of the runtime. The JIT's presence does not imply that every operation or every application spends its time in code the JIT improves.
Compiled code remains part of the runtime's lifetime rules
The Ruby 2.6 MJIT source manages generated code as units associated with instruction sequences. It tracks loaded units and the native handles used for their implementations. Instruction sequences can be garbage collected, so those associations need explicit lifetime handling.
The source coordinates JIT work with garbage collection through hooks and synchronization. It also tracks execution contexts when considering which generated units can be unloaded. A unit still in use cannot be discarded simply because a cache limit calls for reducing loaded code.
This makes the JIT more than a translation pass from Ruby instructions to C. Generated code must coexist with Ruby threads, continuations and collection. Its bookkeeping preserves relationships that ordinary ahead-of-time compilation can settle before the application starts.
The NEWS file also records a Unix waitpid reimplementation to maintain compatibility with processes created for JIT. That is another consequence of adding compiler subprocesses to the runtime. The process-management behavior of the interpreter has to accommodate both application processes and its own compilation work.
These details help explain the experimental designation. A compiler mode needs correct interaction with the surrounding runtime as well as useful generated instructions. Its implementation includes lifetime and process behavior that application source does not directly request.
Short-lived allocation receives separate treatment
The transient heap targets short-lived memory associated with specific classes, including Array and Hash. The announcement also names Object and Struct. This is an allocation improvement within the runtime, separate from enabling MJIT.
The release reports that creating small, short-lived Hash objects is twice as fast in the stated measurement. It also reports a six-to-seven-percent improvement in an RDoc benchmark. The second result is useful because it describes a program-level observation beyond the isolated allocation operation.
Neither number should be applied as a multiplier to an arbitrary service. They establish where the team observed an improvement. The underlying mechanism is still useful to understand because Ruby programs can allocate temporary collection state while evaluating otherwise straightforward expressions.
Block calling has another independent change. Ruby 2.6 speeds up calls through a passed block parameter, building on block-passing work in Ruby 2.5. The announcement reports a 2.6-fold improvement in a microbenchmark for block evaluation.
Proc calls also avoid a temporary allocation associated with the former $SAFE handling. The reported lc_fizzbuzz result is 1.4 times faster. These are distinct calling-path improvements, not measurements of the same effect or proof that the JIT accelerates block-heavy applications by those amounts.
My preference is to keep those explanations separate when assessing the release. A faster allocation path can help code even when native compilation is disabled. A calling-path change can improve a frequent operation without changing the application's syntax. The resulting program behavior depends on how much of its work reaches each path.
Endless ranges express an open endpoint
Ruby 2.6 introduces ranges with no end. The release announcement uses them for taking the remaining elements of an array and for enumeration beginning at a chosen integer. The following example uses a finite array to bound the operation.
items = ["header", "first", "second"]
body = items[1..]
p body
The result is the two elements after the header. The open-ended range expresses the intent without using minus one to name the last element. Here the array operation supplies a finite result even though the range itself has no endpoint.
Using an endless integer range with each is a different operation. Its enumeration continues without a finite endpoint unless the surrounding work stops it. Syntax that is compact for an array slice can therefore describe unbounded iteration in another context.
Enumerable#chain and Enumerator#+ also arrive in this release. NEWS describes their result as an enumerator visiting the receiver's elements and then the arguments' elements in sequence. These APIs make composition of enumerations explicit without requiring one combined source collection.
The language changes preserve Ruby's expression-oriented style while adding more ways to state common operations. Their usefulness is independent of whether MJIT compiles the code using them.
Tooling and experimental APIs have different guarantees
Bundler's inclusion makes dependency tooling available with the standard Ruby distribution. The tagged release identifies version 1.17.2, so the inclusion should not be confused with shipping the separately discussed Bundler 2 transition. RubyGems has its own bundled version, 3.0.1.
The release also introduces RubyVM::AbstractSyntaxTree. It can parse a source string or file and expose nodes with locations and children. NEWS additionally lists obtaining nodes for a proc or method. The announcement explicitly refuses a future-compatibility guarantee for this experimental API.
That naming is worth observing when comparing the final release with preview material. The released module is RubyVM::AbstractSyntaxTree. Tooling written against it must account for its experimental status instead of treating the structure of its nodes as a stable interchange format.
Coverage receives a separate one-shot mode that records whether a line executes at least once. After that first event, the hook is removed for the line. This differs from counting every execution, and the distinction determines which coverage question the mode can answer.
Compatibility changes are part of the release
The tagged notes identify behavior changes beyond new methods. File reading and writing methods no longer invoke an external command merely because a path begins with a pipe character. Pathname's corresponding operations follow the same direction. That changes the interpretation of an argument at a filesystem boundary.
Ruby 2.6 also changes $SAFE into process-global state and permits setting it back to zero. Proc#call no longer changes it. These are compatibility facts connected to the calling-path improvement, rather than a new general isolation mechanism.
The release therefore combines an opt-in experimental compiler with changes that apply to ordinary interpreted execution and standard libraries. A deployment can use the bundled dependency tools and new collection syntax without enabling JIT. Enabling MJIT adds the runtime compiler requirement, while allocation and calling-path improvements retain their separate implementation and workload evidence.