Nithin Bekal About

What's new in Ruby 3.3

17 Dec 2023

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 called it 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.

This article is part of the What's New in Ruby series. To read about a different version of Ruby, pick the version here:

2.3    2.4    2.5    2.6    2.7    3.0    3.1    3.2    3.3

Hi, I’m Nithin! This is my blog about programming. Ruby is my programming language of choice and the topic of most of my articles here, but I occasionally also write about Elixir, and sometimes about the books I read. You can use the atom feed if you wish to subscribe to this blog or follow me on Mastodon.