Once again, a new release of Ruby has arrived along with a few enhancements over last year’s 3.2.0 release. The following is a capture of some of the highlights but definitely dig into the release notes for complete details. Enjoy!
YJIT
As with last year, additional speed improvements have been made with YJIT. We now get a ~15% performance boost, reduced memory usage, and faster warm up over what was seen with 3.2.0. I’ve been using YJIT in production for the past year and looking forward to making use of the latest improvements in the New Year.
We also have RubyVM::YJIT.enable which allows us to programmatically enable YJIT support. This is handy when used with the --disable-yjit and --disable-rjit command line flags to launch your application without JIT support and then enable after to speed up the boot process.
For additional reading, check out the following:
-
Ruby 3.3’s YJIT: Faster While Using Less Memory by Maxime Chevalier-Boisvert and Aaron Patterson.
-
YJIT Is the Most Memory-Efficient Ruby JIT by Maxime Chevalier-Boisvert.
Prism
With this version of Ruby, the Prism gem is now part of Ruby core which provides the following benefits:
-
A modern Ruby syntax parser.
-
More fault tolerant and a replacement to Ripper.
-
Improved API.
For a deeper dive, check out Kevin Newton’s Advent of Prism series of articles.
M:N Thread Scheduler
A new M:N Thread Scheduler has been added for improving threads and/or Ractor performance. This can be broken down as follows:
-
M: The number of ractors.
-
N: The number of native threads.
To quote directly from the original issue, here are the advantages and disadvantages:
-
Advantages
-
We can run M ractors on N native threads simultaneously if the machine has N cores.
-
We can make huge number of Ruby threads or Ractors because we don’t need huge number of native threads.
-
We can support unmanaged blocking operations by locking a native thread to a Ruby thread which issues an unmanaged blocking operation.
-
We can make our own Ruby threads or Ractors scheduler instead of the native thread (OS) scheduler.
-
-
Disadvantages
-
Complicated implementation and it can be hard.
-
Can introduce incompatibility especially on TLS (Thread local storage).
-
We need to maintain our own scheduler.
-
IRB
IRB has gained several improvements and tighter integration with the Reline and Debug gems. For a deeper dive, check out my article on Interactive Ruby.
Performance Warnings
There is now a new warning category for performance related concerns. Example:
warn "A performance example.", category: :performance
This category is meant to warn folks of potential performance concerns for code that isn’t fully optimized. For more on this new category and warnings in general, check out my Ruby Warnings article.
Reline
Subprocesses
The following are deprecated in favor of using IO.popen:
-
`Kernel#open -
`URI.open -
`IO.binread -
`IO.foreach -
`IO.readlines -
`IO.read -
`IO.write
For example, the following is not recommended:
open("| pbcopy", "w") { |clipboard| clipboard.write "demo" }
# warning: Calling Kernel#open is deprecated and will be removed in Ruby 4.0; use IO.popen instead
Instead, you can use .popen without getting warnings:
IO.popen("pbcopy", "w") { |clipboard| clipboard.write "demo" }
Anonymous Parameters
In an unfortunate turn of events — as documented in this issue — anonymous parameters are no longer forwardable to closures within the same method. For example, the following use to work but is now a SyntaxError with this version:
def demo(*) = -> * { puts(*) }
# anonymous rest parameter is also used within block (SyntaxError)
def demo(**) = -> * { puts(**) }
# anonymous keyword rest parameter is also used within block (SyntaxError)
def demo(&) = -> * { puts(&) }
# anonymous block parameter is also used within block (SyntaxError)
💡 For a deeper dive on parameters, check out my article on Method Parameters And Arguments.
Keyword Arguments
You’ll now get a warning when overwriting a previously provided keyword argument. Example:
def demo(label:, content:) = puts "#{label}: #{content}."
demo label: "Demo", content: "A demonstration", label: "Overwrite"
# warning: key :label is duplicated and overwritten on line 2
# Overwrite: A demonstration.
The same applies when splatting keyword arguments:
demo label: "Demo", content: "A demonstration", **{label: "Overwrite"}
# warning: key :label is duplicated and overwritten on line 2
# Overwrite: A demonstration.
In previous versions of Ruby, the original "Demo" value would be silently overwritten with "Overwrite" for label but now you’ll get a proper warning.
Environment
The following environment variables have been introduced:
-
RUBY_GC_HEAP_{0,1,2,3,4}_INIT_SLOTS: Replaces the deprecatedRUBY_GC_HEAP_INIT_SLOTSvariable for configuring Garbage Collection (GC) heap slots. -
REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO: Reduces triggering the GC in order to improve application performance since the more GCs you have, the slower your application is. -
RUBY_MN_THREADS: Allows you to enable M:N threads for the main Ractor. Use1to enable. -
RUBY_MAX_CPU: Allows you to specify the maximum number of native threads for the M:N scheduler. Default: 8. -
RUBY_CRASH_REPORT: Allows you to redirect crash reports to a file or subcommand.
Core
The following breaks down changes to Ruby core syntax:
defined?
The defined? keyword is now optimized for Object Shapes as first mentioned in 3.2.0. For more on keywords, check out my article on Ruby Keywords.
it
Use of Numbered Parameters has been part of Ruby Antipatterns ever since they were introduced in Ruby 2.7.0. With this Ruby release, use of it is being introduced — only as a warning — with full support to be added in Ruby 3.4.0. Until Ruby 3.4.0 is released, use of it without a receiver, argument, or block will issue a warning. Example:
def it = "demo"
def call(&block) = puts yield
call { it }
# warning: `it` calls without arguments will refer to the first block param in Ruby 3.4; use it() or self.it
The reason call { it } is now a warning is because in Ruby 3.4.0, it will be an alias to the _1 numbered parameter. The warning can be corrected, per the warning suggestion, as follows:
# None of these will issue a warning.
call { it() }
call { self.it }
For RSpec, this won’t effect your specifications unless you have a spec defined where it takes no arguments. Otherwise, you’re existing specs will remain unaffected.
Kernel
As with defined?, mentioned above, the following method are optimized: #block_given?, #is_a?, and #instance_of?.
Pay special attention to #is_a? since, with this release, the latest optimization far surpasses the equivalent Ruby Pattern Matching (example): object in Hash. So stick with #is_a? to keep your code performant.
MatchData#named_captures
This method now accepts symbolize_names: true as a keyword argument — which, unfortunately, introduces more Boolean Parameter Control Coupling — that will automatically convert keys as symbols instead of keys as strings (default). Example:
demo = "demo_example"
pattern = /(?<prefix>\w+)_(?<suffix>\w+)/
demo.match(pattern).named_captures
# {
# "prefix" => "demo",
# "suffix" => "example"
# }
demo.match(pattern).named_captures symbolize_names: true
# {
# prefix: "demo",
# suffix: "example"
# }
Module#set_temporary_name
This method allows you to set a temporary name, for debugging purposes, which does not change the permanent name of the module or pollute the global namespace with a constant that might conflict with existing constants. By default, the name of an anonymous class/module is always nil:
demo = Module.new
demo # #<Module:0x00000001241bbe10>
demo.name # nil
Now, with #set_temporary_name, you can provide a temporary name to improve the readability/understandability of your anonymous class/module:
demo = Module.new
demo.set_temporary_name "other"
demo # "other"
demo.name # "other"
When assigning to a constant, the temporary name will be ignored in favor of the constant (as is normal behavior):
Demo = Module.new.set_temporary_name "other"
Demo # Demo
Demo.name # "Demo"
This is also handy when using an anonymous module within your object ancestry, or using the Module Builder Pattern, and you want to provide a readable name for your module:
module Speak
def say = puts "An example."
end
# Yes
demo = Module.new.set_temporary_name ("module:demo").extend Speak
demo # module:demo
demo.name # "module:demo"
Keep in mind that attempting to set a temporary name for a permanent module will result in an error, though:
Demo = Module.new
Demo.set_temporary_name "Bad"
# `set_temporary_name': can't change permanent name (RuntimeError)
Range#overlap?
With ranges, we now have a way to check if an existing range overlaps another range makes simplifies having to use a combination of #cover? and #== to pull this off. Example:
(1..3).overlap? -3.. # true
(1..3).overlap? 2..4 # true
(1..3).overlap? 3.. # true
(1..3).overlap?(..0) # false
(1..3).overlap? 4.. # false
(1..3).overlap? 5 # Integer (expected Range) (TypeError)
(1..3).overlap? nil # NilClass (expected Range) (TypeError)
("a".."z").overlap? "e".."m" # true
Range#reverse_each
In previous versions of Ruby, you could not message #reverse_each on beginning endless range but now you can. Example:
(..5).reverse_each.take 3 # [5, 4, 3]
Range#size
range = 1..5.5r
range # [1, 2, 3, 4, 5]
range.size # 6
The bug, in Ruby 3.2.0 and earlier, was that range.size would report 5 instead of 6 which didn’t account for the Rational endpoint.
Refinement#target
Use of Refinement#refined_class has been deprecated in favor of Refinement#target. For more on refinements in general, check out my Ruby Refinements article.
Special Mentions
The following are not in this version release but are worth mentioning and looking forward to in future versions of Ruby:
-
Reduced memory and improved performance for splats: This will prevent the allocation of parameter splats for improved memory and CPU performance. This also makes them more powerful. For more, check out my article on Method Parameters And Arguments.
Bugs
There are a few bugs with this release that will require a 3.3.1 release soon. Keep an eye on the following:
-
Segmentation fault for Ruby 3.3.0 on aarch64-linux: Effects all
aarch64-linuxplatforms, especially Docker images. You can, temporarily, work around this by applying patch used in the pull request. -
Anonymous arguments are now syntax errors in unambiguous cases: Identifies an issue where you can’t forward anonymous parameters to a nested block. This was too aggressive so will be great to have this functionally restored again.
-
String#match?: Matching strings with a regular expression that uses the case insensitive flag can behave erratically. -
Regexp#match returns nil but allocates T_MATCH objects: Describes a memory performance hit between Ruby 3.2.0 and 3.3.0.
