What's new in Ruby 3.3
Every year on Christmas day, the Ruby core team releases a new version of Ruby. This year will likely be no different, and we can expect Ruby 3.3 next week.
This year, the primary focus of the release is performance and developer experience. There aren’t as many major changes to the language features as in previous versions, so this post is going to be shorter than usual. Here are some of the highlights of the release:
YJIT
YJIT, Ruby’s JIT compiler, has seen some incredible advances in the past couple of years. This year, it has continued getting faster, while also consuming less memory.
Companies like Basecamp and Shopify are already running with preview releases of 3.3 in production and have seen around 15% improvement in average response times, compared to 3.3.0 without YJIT. In fact, YJIT has been so effective at speeding up Rails apps that it will be enabled by default on newly generated Rails apps if you are on Ruby 3.3.
A new method,
RubyVM::YJIT.enable
has been introduced,
which allows you to enable YJIT at runtime.
This can be useful
if you want to load the app quickly
and then enable YJIT
after it has booted.
This is what Rails uses to enable JIT
in an initializer.
RJIT
An experimental JIT compiler called RJIT has been introduced. This is written in pure Ruby, and replaces MJIT, which was an alternative JIT that was available in Ruby 3.2. RJIT uses a lot more memory than YJIT, and exists only for experimentation. In production, you should always use YJIT.
(Update: Here’s an article by the creator of RJIT about why it was added to Ruby.)
IRB
After using pry
and byebug
as my preferred debugging tools for years,
I switched to using IRB
for all my debugging
in the past few months.
With this release,
IRB, and its integration
with the builtin debug
gem
has improved so much
that pry
and byebug
are no longer necessary
for debugging your code.
Range
There are a couple of changes to the Range
class.
An overlap?
method has been added:
(1..3).overlap?(3..5) # true
(1..3).overlap?(4..6) # false
You can also call reverse_each
on begin-less ranges now:
(..10).reverse_each.take(3) #=> [10, 9, 8]
Prism parser
A new parser called Prism has been introduced as a default gem. It is more error tolerant and can be used as a replacement for ripper. This is portable across Ruby implementations, and is currently being integrated into MRI, JRuby, TruffleRuby and Sorbet.
With prism,
you can parse code using
Prism.parse
and get a result that looks like this:
> Prism.parse("1 + 2")
#<Prism::ParseResult:0x000000010f5518c8
@comments=[],
@data_loc=nil,
@errors=[],
@magic_comments=[],
@source=#<Prism::Source:0x000000012aa77530 @offsets=[0], @source="1 + 2", @start_line=1>,
@value=
@ ProgramNode (location: (1,0)-(1,5))
├── locals: []
└── statements:
@ StatementsNode (location: (1,0)-(1,5))
└── body: (length: 1)
└── @ CallNode (location: (1,0)-(1,5))
├── flags: ∅
├── receiver:
│ @ IntegerNode (location: (1,0)-(1,1))
│ └── flags: decimal
├── call_operator_loc: ∅
├── name: :+
├── message_loc: (1,2)-(1,3) = "+"
├── opening_loc: ∅
├── arguments:
│ @ ArgumentsNode (location: (1,4)-(1,5))
│ ├── flags: ∅
│ └── arguments: (length: 1)
│ └── @ IntegerNode (location: (1,4)-(1,5))
│ └── flags: decimal
├── closing_loc: ∅
└── block: ∅,
@warnings=[]>
M:N thread scheduler
A new M:N thread scheduler has been introduced to improve thread and ractor performance. Here, M is the number of ractors and N is the number of native threads (equal to number of CPU cores, for instance). This allows us to create a large number of ractors without having to pay the overhead of creating a native thread for each one of them.
Other changes
- In Ruby 3.4,
there is a proposal
to use
it
as a reference to the first argument to a block. Calling a method calledit
without args has been deprecated and will show a warning. - Bison has been replaced with Lrama parser generator to improve maintainability.
- There have been more optimizations to the garbage collector to further improve performance.
More reading
With these posts, I try to highlight some of the changes that I found most interesting, and often skip over features I might not use. If you’re looking for a more comprehensive look at the release, I highly recommend looking at the release announcement, changelog, and the excellent Ruby References website which covers all the new features in detail with lots of examples.