September 14, 2023·5 min read·engineeringrubyyjitpreviewtooling

Ruby 3.3 preview 2 keeps more execution inside YJIT

YJIT expands compiled call and exception handling, exposes useful statistics in release builds, and accompanies improvements to IRB and library packaging.

A JIT's performance depends on more than the quality of the machine instructions it emits. Execution also has to remain in compiled code when the application reaches common language operations. Ruby 3.3 preview 2 addresses both concerns through broader YJIT coverage and more compact code generation.

The September 14 preview announcement reports compiled exception handlers and says unsupported call types no longer exit to the interpreter. It also makes the ratio_in_yjit statistic available in release builds. Those details make the release interesting as an account of execution coverage and its observation.

I find Ruby's code readable, but its dynamic behavior leaves substantial work to the runtime. A preview that explains which operations can remain compiled gives a more useful performance story than a single headline speedup.

Coverage is part of the compilation problem

The first preview in May already described improved splat and rest arguments and more compiled calls with optional arguments. September continues that work and adds explicit discussion of exception handlers and unsupported call types.

These are common points where Ruby's flexibility reaches the implementation. A method's source can be concise while its arguments have several supported shapes. A JIT needs behavior for those shapes if it is to keep execution on its compiled path.

The announcement's statement about unsupported call types should not be read as a claim that every possible call now receives a specialized implementation. It says those types no longer cause exits to the interpreter. That is a narrower and more specific description of the runtime transition.

Compiled exception handlers extend the model to another part of control flow. Handling an exception is part of program execution, even when an application usually follows its success path. The announcement makes that coverage visible rather than discussing only straight-line arithmetic.

Object shapes and specialized operations remain relevant

The preview continues to report that megamorphic object shapes no longer force instance-variable access back into the interpreter. That extends compiled handling across a case with more varied object layouts.

It also lists specialized operations including integer and string inequality, block presence checks, and type-related predicates. These optimizations target operations for which the runtime can provide specific handling. They do not change the Ruby source contract into a static type system.

The announcement reports register allocation for virtual-machine stack operations. It also describes more compact generated code on ARM64. Together with reduced metadata memory use, those are implementation costs beyond the question of whether a call is supported.

Code size and metadata size need separate attention. Machine code is one allocation associated with JIT compilation, while metadata supports management of that code. Reducing one does not automatically describe the size of the other.

This is why I prefer the individual mechanisms in the preview notes to an undifferentiated performance claim. They identify what changed without asking an application author to assume the same effect for every workload.

Statistics become easier to obtain

The ratio_in_yjit statistic, produced through the YJIT statistics option, is now available in release builds. A special statistics or development build is no longer required for that metric.

ruby --yjit --yjit-stats application.rb

The statistic is relevant to the coverage work because it describes execution associated with YJIT. It does not supply an application's total latency or explain every reason for an exit. It gives one useful observation alongside the runtime changes the release describes.

Exit tracing also supports sampling through trace-exits-sample-rate. This provides another way to investigate execution behavior. The preview lists those diagnostic controls separately from the optimizations, which makes their purpose clearer.

A release-build observation is useful because it lowers the need to maintain a separate diagnostic runtime merely to obtain that particular metric. It still needs interpretation in terms of the application being run. A larger fraction of execution in compiled code and a faster application are related possibilities, not identical measurements.

A benchmark result has a named scope

The announcement reports performance above three times the interpreter on optcarrot. That is a result for the named benchmark, not a guarantee for arbitrary Ruby services or command-line programs.

Its value in this preview is evidence that the implementation changes can produce a substantial result in an actual benchmark. The surrounding details explain why the runtime might improve, including argument handling, specialized operations, and generated-code changes.

The result should remain attached to its benchmark rather than becoming a general migration estimate. The preview does not provide the information needed to assign that factor to an unrelated application.

IRB improves inspection of running code

The preview also updates IRB, including integration with the Ruby debugger through irb:rdbg. The announcement describes an experience comparable to pry-byebug, together with pager support for commands such as ls and show_cmds.

The ls and show_source commands provide more accurate and helpful information. Those changes concern how developers inspect objects and definitions interactively. They complement runtime performance work without being part of the JIT's execution model.

IRB also receives refactoring and bug fixes intended to support further improvements. This is maintenance of an everyday development interface, not a new source-language feature.

A language can remain familiar while its inspection tools improve. For Ruby, where interactive exploration is a natural fit for the object model, clearer source and object information makes the runtime easier to investigate.

Parser availability is not parser replacement

The preview's default-gem list includes yarp 0.9.0. That establishes its distribution in this preview. The announcement separately retains Lrama as the generator replacing Bison for the existing parser workflow.

Those facts should not be collapsed into a claim that YARP has become the default execution parser. A parser library being distributed and the runtime selecting a parser are different integration decisions.

The notes also warn when required libraries are scheduled to become bundled gems in a future Ruby version. That makes future dependency declarations a visible concern before the packaging transition is complete.

Ruby 3.3 remains a preview here. RJIT is still described as experimental and restricted to x86_64 Unix platforms, while YJIT remains the recommended production JIT. The September release adds more coverage and better observation to that production-oriented compiler without changing the stated purpose of the experimental alternative.