STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

プロと読み解くRuby 3.3 NEWS

テクノロジー部門CTO室の笹田(ko1)と遠藤(mame)です。今年の 9 月から STORES 株式会社で Ruby (MRI: Matz Ruby Implementation、いわゆる ruby コマンド) の開発をしています(Rubyのこれからを STORES で作る。Rubyコミッター笹田さん、遠藤さんにCTOがきく「Fun」|STORES People )。お金をもらって Ruby を開発しているのでプロの Ruby コミッタです。

本日 12/25 に、恒例のクリスマスリリースとして、Ruby 3.3.0 がリリースされました(Ruby 3.3.0 リリース)。クックパッド開発者ブログで連載していたように、今年も STORES Product Blog にて Ruby 3.3 の NEWS.md ファイルの解説をします(ちなみに、STORES Advent Calendar 2023 の記事になります。他も読んでね)。NEWS ファイルとは何か、は以前の記事を見てください。

本記事は新機能を解説することもさることながら、変更が入った背景や苦労などの裏話も記憶の範囲で書いているところが特徴です。

Ruby 3.3 は、言語(文法)の変更など、目立つ機能の導入はないのですが、内部の構造がいろいろとよくなっています。ぜひ使ってみてください。

Ruby 3.3 の代表的な変更は次のようなものになります(リリースノートから抜粋)。

  • 新規に書き下ろされた Ruby のパーサーである Prism が導入されました
  • Bison の代わりに、新たに開発されたパーサー生成器である Lrama を使うようになりました
  • Ruby で JIT を実験的に記述するための RJIT が導入されました
  • YJIT に多くの改善が行われました
  • スレッドを軽量に扱うための M:N スレッドスケジューラが導入されました

本記事では、これらを含めて NEWS ファイルにあるものをだいたい紹介していきます。

言語の変更

なんと今回は言語(文法)の変更がありませんでした。

コマンドライン引数などの変更

警告カテゴリに performance が導入された

  • A new performance warning category was introduced. They are not displayed by default even in verbose mode. Turn them on with -W:performance or Warning[:performance] = true. [Feature #19538]

カテゴリ別警告のカテゴリに「性能」が入りました。

コマンドラインオプションで -W:category_name としたり、コード中に Warning[category_name] = true のようにすることで、category に関する警告を有効にする機能が Ruby 3.0 から導入されましたが、これに性能カテゴリ(:performance)が加わりました。性能に無視できない事象が生じたときに出る警告です。デフォルトでは他と同じく無効です。

これで、次のように3つ目の警告カテゴリができたことになります。

  • :deprecated
  • :experimental
  • :performance (NEW!)

ちなみに、性能カテゴリには今はどんなときにこの性能警告が出るかというと、調べた限りでは次のような場合でした。

1) Object Shape の数が限度(現状は8)を超えたとき

$ ruby -W:performance -e 'class C; end; 10.times{o=C.new; 10.times{o.instance_variable_set("@iv#{rand(100)}", 0)}}'
-e:1: warning: The class C reached 8 shape variations, instance variables accesses will be slower and memory usage increased.
It is recommended to define instance variable in a consistent order, for instance by eagerly defining them all in the #initialize method.

2) OpenStruct を使ったとき

$ ruby -W:performance -rostruct -e 'OpenStruct.new{}'
-e:1: warning: OpenStruct use is discouraged for performance reasons

1 の Object Shape の話は、警告文にもあるように、initialize でインスタンス変数をすべて初期化しておくと警告は消せます(が、個人的にはそれは Ruby っぽくないのでいやだなぁ)。

(ko1)

クラッシュレポートをファイルに保存する RUBY_CRASH_REPORT 環境変数が導入された

  • A new RUBY_CRASH_REPORT environment variable was introduced to allow redirecting Ruby crash reports to a file or sub command. See the BUG REPORT ENVIRONMENT section of the ruby manpage for further details. [Feature #19790]

RUBY_CRASH_REPORT という環境変数で、クラッシュレポートを出力するファイルを指定することができるようになりました。

クラッシュレポートとは、処理系(インタプリタ)のバグなどで、あり得ない状態になったとき、処理系のデバッグを助けるために出てくる、「どういう時に起こったか」などを示したものです。

このクラッシュレポートは、例えば SEGV シグナルを受信したときに起こります(アクセスするべきはないメモリにアクセスしてしまった場合におきます)。Ruby プログラムで SEGV シグナルを自分自身に起こすことで、簡単に確認できます。

$ ruby -e 'Process.kill(:SEGV, $$)'
-e:1: [BUG] Segmentation fault at 0x000003e80017b739
ruby 3.3.0dev (2023-12-16T21:45:33Z master d7d10f3ee8) [x86_64-linux]

-- Control frame information -----------------------------------------------
c:0003 p:---- s:0012 e:000011 CFUNC  :kill
c:0002 p:0008 s:0006 e:000005 EVAL   -e:1 [FINISH]
c:0001 p:0000 s:0003 E:001640 DUMMY  [FINISH]

-- Ruby level backtrace information ----------------------------------------
-e:1:in `<main>'
-e:1:in `kill'

-- Threading information ---------------------------------------------------
Total ractor count: 1
Ruby thread count for this ractor: 1

-- Machine register context ------------------------------------------------
 RIP: 0x00007fdc7fd4d75b RBP: 0x000000000000000b RSP: 0x00007fff4f782a48
 RAX: 0x0000000000000000 RBX: 0x0000000000000001 RCX: 0x00007fdc7fd4d75b
 RDX: 0x000000000017b739 RDI: 0x000000000017b739 RSI: 0x000000000000000b
  R8: 0x000000000017b739  R9: 0x00007fdc7f9c9048 R10: 0x00007fdc7fd1df60
 R11: 0x0000000000000206 R12: 0x0000000000000002 R13: 0x00007fdc7f9c9048
 R14: 0x000000000017b739 R15: 0x0000000000000001 EFL: 0x0000000000000206

-- C level backtrace information -------------------------------------------
/home/ko1/ruby/install/master/lib/libruby.so.3.3(rb_print_backtrace+0x14) [0x7fdc8025d1c1]
...(長いので略)
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main) (null):0
[0x555b4d3171d5]

-- Other runtime information -----------------------------------------------

* Loaded script: -e

* Loaded features:

    0 enumerator.so
...(長いので略)
   51 /home/ko1/ruby/install/master/lib/ruby/3.3.0+0/syntax_suggest/core_ext.rb

* Process memory map:

555b4d316000-555b4d317000 r--p 00000000 08:20 4956                       /home/ko1/ruby/install/master/bin/ruby
...(長いので略)
7fff4f7ba000-7fff4f7bc000 r-xp 00000000 00:00 0                          [vdso]

このクラッシュレポート、ワンライナーでもこれだけ出るので、ちょっとしたアプリケーションだとさらに行が膨らみます。また、プロダクション環境で出てきたクラッシュレポートを特別に管理したい、という要求もあるでしょう(現状は stderr に出るので、分けづらかったわけです)。

そこで、Ruby 3.3 からは RUBY_CRASH_REPORT にファイル名を指定し、そのファイルにクラッシュレポートを書き込むことができるようになりました。

$ RUBY_CRASH_REPORT=/tmp/crash_report ruby -e 'Process.kill(:SEGV, $$)'
Aborted

$ ls -la /tmp/crash_report
-rw-r--r-- 1 ko1 ko1 20035 Dec 17 16:15 /tmp/crash_report

ファイル名は %p で PID に置き換えたりできます。

 *   %%    A single % character.
 *   %e    The base name of the executable filename.
 *   %E    Pathname of executable, with slashes ('/') replaced by
 *         exclamation marks ('!').
 *   %f    Similar to %e with the main script filename.
 *   %F    Similar to %E with the main script filename.
 *   %p    PID of dumped process in decimal.
 *   %t    Time of dump, expressed as seconds since the Epoch,
 *         1970-01-01 00:00:00 +0000 (UTC).
 *   %NNN  Octal char code, upto 3 digits.

(ソースコードのコメントから引用)

(ko1)

組み込みクラスのアップデート

未定義の pack テンプレート文字が例外になった

  • Array#pack now raises ArgumentError for unknown directives. [Bug #19150]

Array#packString#unpack は未定義のテンプレート文字を無視していたのですが、Ruby 3.3 からは例外を投げるようになりました。

# Ruby 3.2まで
[1].pack("k") #=> ""

# Ruby 3.3から
[1].pack("k") #=> unknown pack directive 'k' in 'k' (ArgumentError)

なお、Ruby 3.2 では警告が表示されていました。

(mame)

ディレクトリを示すファイルディスクリプタに対するサポートの導入

  • Dir.for_fd added for returning a Dir object for the directory specified by the provided directory file descriptor. [Feature #19347]
  • Dir.fchdir added for changing the directory to the directory specified by the provided directory file descriptor. [Feature #19347]
  • Dir#chdir added for changing the directory to the directory specified by the provided Dir object. [Feature #19347]

ディレクトリを示すファイルディスクリプタ(fd)をサポートするための下記の機能が導入されました。

  • fd から Dir オブジェクトを生成する Dir.for_fd が導入されました。
$ ruby -e 'p Dir.for_fd(Dir.new("/tmp").fileno)'
#<Dir:0x00007f7cc0c64938>
  • fd に対してディレクトリを変更する Dir.fchdir が導入されました。
$ ruby -e 'Dir.fchdir(Dir.new("/tmp").fileno){ p Dir.pwd }'
"/tmp"
  • dir_object.chdir のように、ディレクトリ名ではなく Dir オブジェクトに対してディレクトリを変更するメソッド Dir#chdir が導入されました。
$ ruby -e 'Dir.for_fd(Dir.new("/tmp").fileno).chdir{ p Dir.pwd }'
"/tmp"

そもそもディレクトリのファイルディスクリプタがなんで出てくるんじゃ、という気分になるのですが、UNIX Domain socket 経由で UNIXSocket#send_io を用いてファイルディスクリプタを送る、という機能があり、それでディレクトリを示すファイルディスクリプタを受け取っても Dir オブジェクトを作成できなくて困った、という話だそうです。

(ko1)

Encoding#replicate が削除された

  • Encoding#replicate has been removed, it was already deprecated. [Feature #18949]

おそらく使ったことがある人はいないであろうメソッドのトップ3には入りそうな Encoding#replicate が削除されました。すでに Ruby 3.2 で削除するよっていう警告が出ていたようです。

余談ですが、この変更の背景です。このメソッドは、Ruby で唯一 Encoding を増やすことができるメソッドでした。しかし、これをサポートするためには、文字列エンコーディングは無限に増えるかも、という仮定で文字列エンコーディングを扱わなければなりません。たとえば、Ractor 対応で文字列エンコーディングを格納するテーブルに対する操作時(いくつかの文字列操作時)にわざわざロックをとったりなんなりしないといけなくて、まぁ遅くなる要素満載だったわけです。必要な機能ならしょうがないのですが、まぁ実際にこれが使われることはないわけで、無駄なコストでした。というわけで、Ruby 3.3 では文字列エンコーディングは最大 256 という決め打ちでやっています(これでも多そう)。決め打ちができたことで、例えばエンコーディングテーブルが拡張することがなくなり、物事は大変シンプルになり、性能もよくなっている、と思います。

(ko1)

任意の Fiber を終了させる Fiber#kill が導入された

Fiber#kill が導入されました。Thread#kill に似ているのですが、次のような操作をします。

  1. その Fiber に resume する。
  2. resume 先で、絶対に rescue できない例外(ここでは E)を raise する(これは、インタプリタ終了時に各スレッドで発生する例外とだいたい同じです)。
  3. resume 先で例外 E によってトップレベルまで巻き戻ったら Fiber.yieldFiber#kill 呼び出し側に戻る。この時の返り値は kill 対象の Fiber。

利用例はこんな感じです。

def foo
  Fiber.yield :foo
ensure
  p $! #=> nil
end

f = Fiber.new{ foo }
p resume_result: f.resume #=> {:resume_result=>:foo}
p kill_result: f.kill     #=> {:kill_result=>#<Fiber:0x00007fa04576d58 ... (terminated)>}

ちなみに、E は特殊な例外なので、$! などでは見えないようになっています。

(ko1)

MatchData#named_capturessymbolize_names キーワードが指定できるようになった

  • MatchData#named_captures now accepts optional symbolize_names keyword. [Feature #19591]

MatchData#named_captures は String キーのハッシュを返しますが、symbolize_names: true を指定すると、Symbol キーのハッシュを返すようになります。

"foobar" =~ /(?<a>...)(?<b>...)/

p $~.named_captures                        #=> {"a"=>"foo", "b"=>"bar"}
p $~.named_captures(symbolize_names: true) #=> {:a=>"foo", :b=>"bar"}

symbolize_names というキーワード名は、JSON.parse の既存のキーワードにならっています。でもちょっとわかりにくい気もする。

(mame)

匿名モジュールに仮の名前を付ける Module#set_temporary_name が追加された

  • Module#set_temporary_name added for setting a temporary name for a module. [Feature #19521]

無名モジュールや無名クラスに仮の名前を与える API が追加されました。

c = Class.new
p c #=> #<Class:0x00007fd396627778>

# 仮の名前を与える
c.set_temporary_name("fake_name")

# 表示名が変わる
p c #=> fake_name

無名クラスを活用するフレームワークで、クラスの名前をわかりやすく表示したい、というユースケースのために入ったようです。

おもしろポイントとして、クラス名っぽい名前を与えることはややこしいので禁止されています。

c.set_temporary_name("FakeName")
#=> the temporary name must not be a constant path to avoid confusion (ArgumentError)

また、定数に代入した時点で、仮の名前は消えます。そして、ちゃんとした定数名が与えられたあとに仮の名前を与えることはできません。

Foo = c
p c #=> Foo

c.set_temporary_name("fake_name_again")
#=> can't change permanent name (RuntimeError)

(mame)

ObjectSpace::WeakKeyMap が導入された

  • ObjectSpace::WeakKeyMap
    • New core class to build collections with weak references. The class use equality semantic to lookup keys like a regular hash, but it doesn't hold strong references on the keys. [Feature #18498]

キーに対してのみ weak な WeakKeyMap が導入されました。つまり、key に登録したオブジェクトが、ここ以外から参照されていないことがわかった場合、テーブルから当該エントリを削除するというものです(ちなみに、保守的GCのため、プログラム上で参照がないように見えても消えないことがあります)。

元々あった WeakMap は、key もしくは value に対して weak reference を持つもので、どちらかが GC されたらエントリがなくなる、というものでした。

tab = ObjectSpace::WeakMap.new

key = Object.new
val = Object.new
tab[key] = val
val = nil # val has no reference

while tab.size > 0
  10_000.times{Object.new} # val が GC されるので、tab.size は 0 に
end

p tab.size #=> 0

今回導入された WeakKeyMap は key に対してのみ weak reference を持つので、key が存在する限り、value は GC されないことになります。

tab = ObjectSpace::WeakKeyMap.new

key = Object.new
val = Object.new
tab[key] = val
p tab #=> #<ObjectSpace::WeakKeyMap:0x000002876411d468 size=1>
val = nil # val has no reference, key has a reference from the local variable

while /size=1/ =~ tab.inspect # #size がないので inspect の結果を見る
  10_000.times{Object.new}
end

p tab # ここには到達しない

WeakMap は identity でキーを引きますが(Hash の compare_by_identity したものと同じ、#equal?(other) が true になるかどうか) 、WeakKeyMap は普通の Hash と同じく equality (#eql?(other) が true になるかどうか)でキーを引きます。

tab = ObjectSpace::WeakMap.new
tab[[1,2]] = [3, 4]
p tab[[1, 2]]         #=> nil, key とは別の配列なのでヒットしない

ktab = ObjectSpace::WeakKeyMap.new
ktab[[1,2]] = [3, 4]
p ktab[[1, 2]]        #=> [3, 4], key と eql? なのでヒット

例えば、キャッシュを作るために便利なのではないか、ということです。

ちなみに、#getkey というメソッドがあり、実際に key に利用されているオブジェクトを取り出すことができます。

ktab = ObjectSpace::WeakKeyMap.new
ktab[key = [1,2]] = [3, 4]
gotkey = ktab.getkey(query = [1, 2])
p gotkey #=> [1, 2]
p key.equal?(gotkey) #=> true
p key.equal?(query) #=> false

(ko1)

ObjectSpace::WeakMap#delete が導入された

  • ObjectSpace::WeakMap#delete was added to eagerly clear weak map entries. [Feature #19561]

エントリを消す ObjectSpace::WeakMap#delete が導入されました。

(ko1)

Proc#dupProc#clone がちゃんと initialize フックを呼ぶようになった

  • Now Proc#dup and Proc#clone call #initialize_dup and #initialize_clone hooks respectively. [Feature #19362]

Proc#dupProc#clone が、それぞれ #initialize_dup#initialize_clone を呼ぶようになりました。これまで呼んでなかったんですね。でも、気にする人はほとんど居なさそう。

(ko1)

アプリケーション初期化終了時に呼ぶ Process.warmup が導入された

  • New Process.warmup method that notify the Ruby virtual machine that the boot sequence is finished, and that now is a good time to optimize the application. This is useful for long-running applications. The actual optimizations performed are entirely implementation-specific and may change in the future without notice. [Feature #18885]

アプリケーションの起動準備、たとえば必要な require が終わったときに呼び出す Process.warmup が導入されました。具体的に何をするかは今後のバージョンで変わってきそうですが、とりあえず今は調べた限り、次のようなことをするようです。

 *  * Performs a major GC.
 *  * Compacts the heap.
 *  * Promotes all surviving objects to the old generation.
 *  * Precomputes the coderange of all strings.
 *  * Frees all empty heap pages and increments the allocatable pages counter
 *    by the number of pages freed.
 *  * Invoke +malloc_trim+ if available to free empty malloc pages.

(コメントから引用)

全部 GC 関連ですね。fork しやすいようにいろいろリセットしているような気がします(文字列操作だけ、ちょっと違うかな)。多分、Rails とかフレームワークがやるので、あんまり気にしなくていいんじゃないですかね。

(ko1)

Process::Status#&#>> が非推奨になった

  • Process::Status#& and Process::Status#>> are deprecated. [Bug #19868]

端的にいえば、$? を整数っぽく扱うことが非推奨になりました。

system("echo")
p $? #=> #<Process::Status: pid 964544 exit 0>

p $? & 1
#=> warning: Process::Status#& is deprecated and will be removed in Ruby 3.4; use other Process::Status predicates instead

p $? >> 1
#=> warning: Process::Status#>> is deprecated and will be removed in Ruby 3.4; use other Process::Status attributes instead

歴史としては、Ruby 1.8.0 までは $? は終了コードの整数そのものでした。それが 1.8.1 から Process::Status のインスタンスになったので、互換性のため、Process::Status は整数っぽく振る舞うことが期待されたようです。とはいえ、Integer をフルに模倣するほどの必要はなさそうということで、実際に使われる可能性のありそうな &>> だけ互換メソッドが提供されたようです。

しかし、終了コードは環境によって異なることから、そもそも &>> を使うとポータブルに判定をするプログラムを書くことができないということが指摘され、推奨されないことになりました。

(mame)

Range を逆順にたどる Range#reverse_each が追加された

  • Range#reverse_each can now process beginless ranges with an Integer endpoint. [Feature #18515]
  • Range#reverse_each now raises TypeError for endless ranges. [Feature #18551]

(1..10000).reverse_each { ... } と書いたとき、Ruby 3.2 までは Enumerable#reverse_eachを呼び出していました。これは、一旦 (1..10000) をメモリ上で配列に展開した上で、最後の要素からたどるものでした。

この方式にはいくつか問題がありました。

  • 巨大な配列を作る可能性があり、効率が悪い
  • beginless range に対して使うと例外が出る(先頭がなく、配列に展開できないので)
  • endless range に対して使うとメモリを食いつぶす(無限長の配列を作ろうとするので)

そこで、これらをうまく扱える Range 専用の特化メソッド Range#reverse_each が導入されました。

# 配列に展開しないので大きな範囲の Range でも動く
p (0..100000000000).reverse_each.first #=> 100000000000

# beginless range に対しても動く
p (..10000).reverse_each.first #=> 10000

# endless range に対しては例外を投げる(メモリを食いつぶさない)
p (0..).reverse_each.first #=> can't iterate from NilClass (TypeError)

(mame)

範囲同士が重複しているかチェックする Range#overlap? が追加された

  • Range#overlap? added for checking if two ranges overlap. [Feature #19839]

2 つの Range に重複する部分があるかどうかを判定する Range#overlap? メソッドが追加されました。

p (0..2).overlap?(1..3) #=> true
p (0..2).overlap?(3..5) #=> false

もともと ActiveSupport にあったメソッドを、Ruby 本体に逆輸入したような形です。

以下余談。

単純なメソッド追加に思えるかもしれませんが、議論はかなり盛り上がりました。というのも、本体に入るとなると細かいところまで実装が精査されがちで、今回は次のコーナーケースが指摘されたからです。

p (0...1).overlap?(1...1) #=> ActiveSupport の overlap? は true を返す

(1...1) は空なので重複は存在し得ないのに、ActiveSupport は true を返してしまっています。

これを正しく判定するためには、「Range#empty? が必要なのではないか?」となりました。そして放置されていた過去の [Feature #13933] に脚光が当たりました。すると今度は「(...-Float::INFINITY) が空であることは判定できるのか?」となりました。つまり、-Float::INFINITYは Float の範囲で最小の値であること(それより小さい Float は存在しないこと)を知らないと、(...-Float::INFINITY) が空であることに気づけない。ということで、「さらに Float#minimum? のようなメソッドも必要になるのでは?」と言った感じ。

最終的に Matz の判断で、

  • Range#overlap? は入れる
  • Range#empty?Float#minimum? は入れない
  • Range#overlap? は「最小の値」というのが存在しないという前提で振る舞う

ということになりました。最後の条件のために、次の判定は true を返します。

p (...-Float::INFINITY).overlap?(...0) #=> true

(...-Float::INFINITY) は空なので、実際には重複はないと考えられるにもかかわらず、true を返すことにご注意ください。このことは Range#overlap? のドキュメントに書かれています。

まあ、実際にハマる人はほぼいないと思いますが……。

このあたりはコミッタの akr さんがだいぶ深く考察や実験をしていたので、興味のある方のためにリンクを張っておきます。

(mame)

Refinement#refined_class#target に名前変更された

  • Add Refinement#target as an alternative of Refinement#refined_class. Refinement#refined_class is deprecated and will be removed in Ruby 3.4. [Feature #19714]

Refinement が refine する対象を返すメソッド Refinement#refined_class が Ruby 3.2 で追加されたのですが、これが #target に名前変更されることになりました。

module Foo
  refine(Array) { def second = self[1] }
end

Foo.refinements              #=> [#<refinement:Array@Foo>]
Foo.refinements.first.target #=> Array

Ruby 3.3 では #refined_class も使えますが、警告が出ます。Ruby 3.4 では使えなくなる予定です。

以下は小話です。

名前変更の理由は、refine される対象はクラスとは限らず、モジュールかもしれないからです。

というと、「Class は Module のサブクラスなんだから、#refine_module でよいのでは?」と思うかもしれません。実際、この変更の最初の提案は #refine_module でした。

しかし Ruby の設計者である Matz が言うには、Class が Module のサブクラスであるのは歴史的経緯にすぎず、クラスとモジュールは別物と考えたい、とのことでした。実際、Ruby の国際規格である ISO/IEC 30170 では、いたるところで "class or module" のように並列して言及されている(まとめて module と言及しないようにしている)とのことです。

ということで、今後は「Ruby ではクラスはモジュールの一種」などとは言わない方がよいかもしれません。

(mame)

正規表現エンジンのReDoS対策範囲が広がった

  • Regexp
    • The cache-based optimization now supports lookarounds and atomic groupings. That is, match for Regexp containing these extensions can now also be performed in linear time to the length of the input string. However, these cannot contain captures and cannot be nested. [Feature #19725]

Ruby 3.2 で入った ReDoS 対策のメモ化最適化が、Ruby 3.3 では先読み・後読みやアトミックグループまでサポートしました。

# Ruby 3.2 の ReDoS 対策の例(Ruby 3.1 では約 10 秒 → Ruby 3.2 では一瞬)
/^(a|a)*$/ =~ "a" * 28 + "z"

# 先読みを含む例(Ruby 3.2 では約 10 秒 → Ruby 3.3 では一瞬)
/^(a|a)*(?=b)/ =~ "a" * 28 + "zb"

ただし、先読みなどの中でキャプチャを行う正規表現 /^(a|a)*(?=b(c)d)/ は最適化対象にならないので注意してください。

これで、実用上よく書かれる正規表現の大半は最適化対象に入った感じがありますね。

(mame)

String#bytesplice がコピー元の部分範囲を指定できるようになった

  • String#bytesplice now accepts new arguments index/length or range of the ource string to be copied. [Feature #19314]

String#bytesplice は、文字列の指定した範囲に別の文字列を埋め込むメソッドで、Ruby 3.2 で追加されました。インデックスは文字単位ではなくバイト単位で指定されます。

# Ruby 3.2 でも 3.3 でも動く
src = "ABC"
dst = "0123456"
dst.bytesplice(2..4, src)
p dst #=> "01ABC56"

Ruby 3.3 では、コピー元の文字列をまるごと埋め込むのではなく、指定した範囲だけ埋め込むことができるようになりました。

# Ruby 3.3 で動く
src = "ABCDEF" # この最初の 3 文字だけを埋め込みたい
dst = "0123456"
dst.bytesplice(2..4, src, 0..2)
p dst #=> "01ABC56"

dst.bytesplice(2..4, src[0..2]) としても意味は同じなのですが、一旦オブジェクトを作ることになるのが嫌とのことでした。

なお、この範囲は Range ではなく、開始位置と長さの整数でも指定できます。

src = "ABCDEF"
dst = "0123456"
dst.bytesplice(2, 3, src, 0, 3)
p dst #=> "01ABC56"

ちなみに、Range と整数の指定を混ぜることはできません。つまり dst.bytesplice(2, 3, src, 0..2) はエラー。引数の数は 2 個 or 3 個 or 5 個でなくてはならない。ややこしいですね。

(mame)

Thread::Queue#freezeがエラーになった

  • Thread::Queue#freeze now raises TypeError. [Bug #17146]
  • Thread::SizedQueue#freeze now raises TypeError. [Bug #17146]

Thread::Queue(と Thread::SizedQueue)オブジェクトを freeze すると、TypeError が出るようになりました。まぁ、誰もやらないような気がするのですが、あるオブジェクトを再帰的に全部 freeze しよう、なんてことをするとはまるかもしれません。

以下、余談な背景です。そもそもの報告は、freeze した Queue オブジェクトに enqueue できるのはどういうことであるか(FrozenError を返すべき)、というものでした。Queue を配列のようなデータの入れ物として考えると、それはそれで妥当な主張に見えます。が、frozen な Queue ってなんの意味があるのか、とか、Queue#pop で待っている他のスレッドがいたときにどうするべきか、とか、そもそも Queue は機能であり裏側にあるデータ構造とは無関係(例えば Binding とかも似たような話になります)とか、なんか色々議論が出たので、そもそも freeze しちゃいかん、という話になりました。実は ENV オブジェクトも freeze しようとしたらエラーになります(freeze できません)。これは、ENVはプロセスの属性へのアクセサであり、環境変数の管理自体に責任を持っているからではない、という理屈から、なんじゃないかなぁ。

(ko1)

Time.new の引数の制限が厳しくなった

  • Time.new with a string argument became stricter. [Bug #19293]

文字列をもとに Time.new を行うとき、時刻指定が必須になりました。

Time.new("2023-12")
#=> Ruby 3.2 では 2023-12-01 00:00:00 +0900
#=> Ruby 3.3 では no time information (ArgumentError)

Time.new('2023-12-20')
#=> Ruby 3.2 では 2023-12-20 00:00:00 +0900
#=> Ruby 3.3 では no time information (ArgumentError)

ただし、年だけ指定した Time.new("2023") は許されるので注意。複数引数で Time を初期化する Time.new(yyyy, [mm, [dd, [HH, [MM, [SS]]]]]) が文字列を昔から(Ruby 1.9.2 から)許容していたので、それとの互換性を保つためにこのようになりました。

Time.new("2023", "12", "20")
#=> Ruby 3.2 でも 3.3 でも 2023-12-20 00:00:00 +0900

Time.new("2023", "12")
#=> Ruby 3.2 でも 3.3 でも 2023-12-01 00:00:00 +0900

Time.new("2023")
#=> Ruby 3.2 でも 3.3 でも 2023-01-01 00:00:00 +0900

(mame)

TracePointrescue イベントが追加された

  • TracePoint

    • TracePoint supports rescue event. When the raised exception was rescued, the TracePoint will fire the hook. rescue event only supports Ruby-level rescue. [Feature #19572]

resuce 節を開始するとき(例外クラスにマッチしたとき)に発火する :rescue イベントが導入され、フックを入れることができるようになりました。

TracePoint.trace(:rescue){|tp| p tp}

begin
  begin
    raise
  rescue
    p :reachable1
    raise
  end
rescue SyntaxError
  # 実行されないので :rescue イベントは起こらない
  p :unreachable
rescue
  p :reachable2
end

実行結果:

#<TracePoint:rescue t.rb:7>
:reachable1
#<TracePoint:rescue t.rb:14>
:reachable2

デバッガでブレイクポイントを入れたい、みたいな話のようです(が、私はいまいちわかっていません)。

ちなみに、Ruby で rescue を記述したものに限り発火するので、例えば C extension が rb_rescue() を使って例外をトラップしたようなケースでは発火しません。

(ko1)

stdlibのアップデート

多くの default gem が bundled gem に昇格する予定という警告を出すようになった

  • RubyGems and Bundler warn if users do require the following gems without adding them to Gemfile or gemspec. This is because they will become the bundled gems in the future version of Ruby. This warning is suppressed if you use bootsnap gem. We recommend to run your application with DISABLE_BOOTSNAP=1 environmental variable at least once. This is limitation of this version. [Feature #19351] [Feature #19776] [Feature #19843]
  • Targeted libraries are:
    • abbrev
    • base64
    • bigdecimal
    • csv
    • drb
    • getoptlong
    • mutex_m
    • nkf
    • observer
    • racc
    • resolv-replace
    • rinda
    • syslog

上記の gem は、Ruby 3.4 で default gem から bundled gem に昇格される予定です。そのため、これらを使っているユーザは次の対応を取る必要があります。

  • これらの gem に依存している gem では、gemspec で依存を明示する必要がある
  • これらの gem に依存しているアプリケーションでは、Gemfile で依存を明示する必要がある

その対応が必要であることをユーザに知らせるために、これらの gem を(gemspec や Gemfile に追加なしに)require したら警告が出るようになりました。

$ cat Gemfile
source "https://rubygems.org"

$ cat test.rb
require "base64"

$ bundle exec ruby test.rb
/tmp/foo/test.rb:1: warning: base64 which will no longer be part of the default gems since Ruby 3.4.0. Add base64 to your Gemfile or gemspec.

Gemfile に gem "base64" を書けば警告は消えます。

$ cat Gemfile
source "https://rubygems.org"

gem "base64" # これを追加

$ cat test.rb
require "base64"

$ bundle exec ruby test.rb

なお、実装上の都合で、bootsnap が有効だとこの警告は出ないとのことです。常に bootsnap を使っている人は、DISABLE_BOOTSNAP=1 で試すと良いでしょう。

(mame)

Socket#recv系のメソッドが、ストリームが閉じられているときに nil を返すようになった

  • Socket#recv and Socket#recv_nonblock returns nil instead of an empty string on closed connections. Socket#recvmsg and Socket#recvmsg_nonblock returns nil instead of an empty packet on closed connections. [Bug #19012]

クローズされた Socket に対して #recv#recv_nonblock#recvmsg#recvmsg_nonblock を呼んだとき、nil が返されるようになりました。

require "socket"

r, w = UNIXSocket.socketpair
w.close

p r.recv(1)
# Ruby 3.2 では ""
# Ruby 3.3 では nil

チケットに動機が書かれていなかったので、提案者の byroot に聞いてみました。「pitchforkpipe から socketpair に書き換えたところ、read_nonblock と recvmsg_nonblock の挙動が違って(前者は closed のとき nil を返し、後者は "" を返していた)バグっぽかったから」とのことでした。今回の変更で、どちらも nil を返すようになりました。

(mame)

名前解決が割り込み可能になった

  • Name resolution such as Socket.getaddrinfo, Socket.getnameinfo, Addrinfo.getaddrinfo, etc. can now be interrupted. [Feature #19965]

名前解決が割り込み可能になりました。Ruby 3.2までは、DNS サーバの不調などで名前解決ができないとき、Ctrl+C を押しても効かず、タイムアウトするまで待たされていました。

$ ruby -rsocket -e 'Addrinfo.getaddrinfo("www.ruby-lang.org", 80)'
^C^C^C^C

Ruby 3.3では、ちゃんと割り込めるようになりました。

$ ruby -rsocket -e 'Addrinfo.getaddrinfo("www.ruby-lang.org", 80)'
^C-e:1:in `getaddrinfo': Interrupt
        from -e:1:in `<main>'

以下、経緯。

Ruby は名前解決に getaddrinfo(3) という C 関数を使っています。これは同期的に名前解決をするもので、別スレッドからキャンセルする方法もありません。なので、Ctrl+C のような割り込みやタイムアウトで止めることができませんでした。

そこで、かつて Ruby は、非同期でキャンセル可能な getaddrinfo_a(3) という C 関数 を試しました。しかしこの API は fork(2) と組み合わせて使えないという問題がありました。Ruby エコシステムでは unicorn や puma などが fork(2) を活用しているので、この問題は致命的。ということで、あえなくリバートされました。[Bug #17220]

それから 3 年。さすがになんとかしたいということで、getaddrinfo_a(3) ではなく c-ares という非同期名前解決ライブラリを使って名前解決をするパッチを書いてみました。しかし残念ながら、c-ares はシステム依存の名前解決に対応しておらず、互換性に難があるということで、これは見送りになりました。[Feature #19430]

そして次は getaddrinfo(3) を別スレッドの中で動かすというアプローチでパッチを作りました。スレッドを作るので名前解決が少し遅くなりますが、従来どおり getaddrinfo(3) を使うので互換性の問題はありません。Ruby 3.3 にはこのパッチが入っています。[Feature #19965]

なお今回のパッチは、getaddrinfo(3) を呼ぼうとするたびにスレッドを作るので、スレッドプールを作るなどの改良が考えられます。スレッドプログラミングに自信ある人は挑戦してみるといかがでしょうか。遠藤は毎回スレッドを作る単純なアプローチでもかなり消耗したので、やりたくありません。

(mame)

Random::Formatter#alphanumeric が使用文字種を指定できるようになった

  • Random::Formatter#alphanumeric is extended to accept optional chars keyword argument. [Feature #18183]

端的に言えば、SecureRandom.alphanumeric で使用する文字種を指定できるようになりました。

require "securerandom"

SecureRandom.alphanumeric(10, chars: ["A", "B", "C"])  #=> "ACBAABBCBC"

Random::FormatterSecureRandom だけでなく Random にも include/extend されているので、Random#alphanumeric も使えます。

この機能は 2 年前に提案され、機能自体はすぐに承認されたのですが、良いメソッド名の模索に時間がかかりました。

(mame)

新しいパーサ Prism が導入された

  • Introduced the Prism parser as a default gem
    • Prism is a portable, error tolerant, and maintainable recursive descent parser for the Ruby language
  • Prism is production ready and actively maintained, you can use it in place of Ripper
    • There is extensive documentation on how to use Prism
    • Prism is both a C library that will be used internally by CRuby and a Ruby gem that can be used by any tooling which needs to parse Ruby code
    • Notable methods in the Prism API are:
      • Prism.parse(source) which returns the AST as part of a parse result object
      • Prism.parse_comments(source) which returns the comments
      • Prism.parse_success?(source) which returns true if there are no errors
  • You can make pull requests or issues directly on the Prism repository if you are interested in contributing
  • You can now use ruby --parser=prism or RUBYOPT="--parser=prism" to experiment with the Prism compiler. Please note that this flag is for debugging only.

Prism という新しい Ruby パーサライブラリが導入されました。

Prism の使い方

もっとも基本的な使い方は、Prism.parse メソッドに Ruby コードを渡すことです。

require "prism"

pp Prism.parse("100.times { }")

上を実行すると、ProgramNode を頂点とする抽象構文木(Abstract Syntax Tree; AST)が出力されるのがわかります。

#<Prism::ParseResult:0x00007fe421b75478
 @comments=[],
 @data_loc=nil,
 @errors=[],
 @magic_comments=[],
 @source=#<Prism::Source:0x00007fe421a2d908 @offsets=[0], @source="100.times { }", @start_line=1>,
 @value=
  @ ProgramNode (location: (1,0)-(1,13))
  ├── locals: []
  └── statements:
      @ StatementsNode (location: (1,0)-(1,13))
      └── body: (length: 1)
          └── @ CallNode (location: (1,0)-(1,13))
              ├── flags: ∅
              ├── receiver:
              │   @ IntegerNode (location: (1,0)-(1,3))
              │   └── flags: decimal
              ├── call_operator_loc: (1,3)-(1,4) = "."
              ├── name: :times
              ├── message_loc: (1,4)-(1,9) = "times"
              ├── opening_loc: ∅
              ├── arguments: ∅
              ├── closing_loc: ∅
              └── block:
                  @ BlockNode (location: (1,10)-(1,13))
                  ├── locals: []
                  ├── locals_body_index: 0
                  ├── parameters: ∅
                  ├── body: ∅
                  ├── opening_loc: (1,10)-(1,11) = "{"
                  └── closing_loc: (1,12)-(1,13) = "}",
 @warnings=[]>

この AST を辿って、レシーバの 100 を取り出してみましょう。

require "prism"

res = Prism.parse("100.times { }")

# func メソッドの呼び出しノードを取り出す
call_node = res.value.statements.body.first

# メソッド呼び出しのレシーバのノードを取り出す
pp call_node.receiver
#=> @ IntegerNode (location: (1,0)-(1,3))
#   └── flags: decimal

# ソースコード中で対応する部分文字列を取り出す
pp call_node.receiver.slice #=> "100"

Ruby プログラムを機械的にいじれます。なにかできそうな気がしてきませんか?

コメントを取り出す例。

require "prism"

comments = Prism.parse_comments(<<END)
# foo
100.times { } # bar
# baz
END

comments[0].location.slice #=> "# foo"
comments[1].location.slice #=> "# bar"
comments[2].location.slice #=> "# baz"

また、AST を文字列にシリアライズ・デシリアライズする Prism.dump / Prism.load という API もあります。

require "prism"

src = "100.times { }"
dump = Prism.dump(src)

p dump #=> "PRISM\x00\x12\x00\x00..."

pp Prism.load(src, dump) #=> #<Prism::ParseResult:0x00007f7584eb9118 ...>

シリアライズのフォーマットは https://github.com/ruby/prism/blob/main/docs/serialization.md で公開されています。Kevin Newton に用途を聞いたところパース結果をキャッシュしたい場合や、Ruby の AST を Ruby 以外の言語で扱いたい場合などに便利とのことです。

(参考)Ruby パーサの比較

Ruby から使える Ruby のパーサは他にもいくつかあるのですが、自分(遠藤)が知っている範囲の独断と偏見で比較しておきます。ご参考まで。

  • RubyVM::AbstractSyntaxTree.parse
    • Ruby 本体のパーサをラップした、本体組み込みの機能。
    • ノード構造:しばしば、わかりにくい。
      • Ruby 1.8 時代からの文法拡張の積み重ねで、ノード名がわかりにくかったり、ノード構造が奇妙だったり。
    • 情報の完全性:コメントや括弧の文字の情報など、具象情報は落ちている。
      • トークン列を得る API を組み合わせて使うことで、気合で探せないこともないが、正直めんどうくさい。
      • パーサが簡単な最適化をするため、あるべきノードが消えることもあった(なお、Ruby 3.3 でパーサでの最適化はしなくなったはず)。
    • 利便性:子ノードをたどるのに便利なアクセサなどはない。
      • .children[n]、つまり「子ノードの配列の n 番目」という指定でたどる。やはり気合が必要。
    • 互換性:Ruby 本体のパーサそのものなので、もっとも確実。
    • 速度:速い。
  • ripper gem
    • Ruby 本体のパーサのソースコードに相乗りする形で実装された Ruby パーサライブラリ。
    • ノード構造:Ruby 本体とほぼ同じ。つまりわかりにくい。
    • 情報の完全性:正直よく知らない(基本的には Ruby 本体と同じはずだけど、一部補完している気もする)。
    • 利便性:Ripper.sexp は Ruby の配列で AST を表現する。シンプルだけど、やはり子ノードをたどる便利なアクセサなどはない(はず)。
    • 互換性:本体のパーサの構文規則を流用してはいるものの、実装方式が微妙に違うため、実は細かいところで互換性がないこともある。
    • 速度:たぶん速い。
  • parser gem
    • Ruby 本体のパーサのソースコードをコピペ・改変する形で実装された Ruby パーサライブラリ。
    • ノード構造:Ruby 本体と同じ(はず)。
    • 情報の完全性:コメントや括弧など、具象情報も細かく残っている(はず)。
    • 利便性:子ノードをたどる便利なアクセサがある。また、おそらく(Ruby 本体のパーサを除けば)一番使われている Ruby パーサで、ほかにも便利機能が充実している(コメントをASTノードに紐付ける機能や、TreeRewriterなど)。
    • 互換性:あまり知らないですが、結構頑張っていそうな気がする。
    • 速度:Ruby で書かれているので遅い。
  • prism gem
    • スクラッチから手書きで作り直されている Ruby パーサライブラリ。
    • ノード構造:AST もスクラッチから設計されていて、比較的わかりやすいと思う。
    • 情報の完全性:コメントや括弧など、具象情報も細かく残っている。
    • 利便性:子ノードをたどる便利なアクセサがある。
    • 互換性:個人的な印象では、枯れてはいない。(現時点の)Ruby 本体のパーサとは非互換があったり、しばしばバグがあったりはする。今後直っていくことに期待。
    • 速度:C言語で書かれているので速い。

なお、Prism はただのライブラリではなく、Ruby 本体のパーサを Prism で置き換えることを目指して開発が進められています。Ruby 3.3 では、--parser=prism というコマンドラインオプションを渡すことで、インタプリタが従来のパーサではなく Prism を使うようになります。ただし、Prism が出力した AST を Instruction Sequence(Ruby VMのバイトコード)に変換する部分はまだ実験的ということです。Prism 開発者以外には当面意味がないオプションなので、覚えなくてもよいでしょう。

Prism については、メインの作者である Kevin Newton がアドベントカレンダーを書いていたので、興味ある方はご参照ください。

kddnewton.com

(mame)

標準添付のライブラリがいろいろ更新された

The following default gems are updated.

  • RubyGems 3.5.3
  • abbrev 0.1.2
  • base64 0.2.0
  • benchmark 0.3.0
  • bigdecimal 3.1.5
  • bundler 2.5.3
  • cgi 0.4.1
  • csv 3.2.8
  • date 3.3.4
  • delegate 0.3.1
  • drb 2.2.0
  • english 0.8.0
  • erb 4.0.3
  • error_highlight 0.6.0
  • etc 1.4.3
  • fcntl 1.1.0
  • fiddle 1.1.2
  • fileutils 1.7.2
  • find 0.2.0
  • getoptlong 0.2.1
  • io-console 0.7.1
  • io-nonblock 0.3.0
  • io-wait 0.3.1
  • ipaddr 1.2.6
  • irb 1.11.0
  • json 2.7.1
  • logger 1.6.0
  • mutex_m 0.2.0
  • net-http 0.4.0
  • net-protocol 0.2.2
  • nkf 0.1.3
  • observer 0.1.2
  • open-uri 0.4.1
  • open3 0.2.1
  • openssl 3.2.0
  • optparse 0.4.0
  • ostruct 0.6.0
  • pathname 0.3.0
  • pp 0.5.0
  • prettyprint 0.2.0
  • pstore 0.1.3
  • psych 5.1.2
  • rdoc 6.6.2
  • readline 0.0.4
  • reline 0.4.1
  • resolv 0.3.0
  • rinda 0.2.0
  • securerandom 0.3.1
  • set 1.1.0
  • shellwords 0.2.0
  • singleton 0.2.0
  • stringio 3.1.0
  • strscan 3.0.7
  • syntax_suggest 2.0.0
  • syslog 0.1.2
  • tempfile 0.2.1
  • time 0.3.0
  • timeout 0.4.1
  • tmpdir 0.2.0
  • tsort 0.2.0
  • un 0.3.0
  • uri 0.13.0
  • weakref 0.1.3
  • win32ole 1.8.10
  • yaml 0.3.0
  • zlib 3.1.0

The following bundled gem is promoted from default gems.

  • racc 1.7.3

The following bundled gems are updated.

  • minitest 5.20.0
  • rake 13.1.0
  • test-unit 3.6.1
  • rexml 3.2.6
  • rss 0.3.0
  • net-ftp 0.3.3
  • net-imap 0.4.9
  • net-smtp 0.4.0
  • rbs 3.4.0
  • typeprof 0.21.9
  • debug 1.9.1

たくさん更新されました! 個々の差分はさすがに把握していないので、各自でお調べください……。

(mame)

サポートプラットフォーム

今回、とくにサポートプラットフォームの追加や削除はありません。

非互換

open("| command") が非推奨になった

  • Subprocess creation/forking via the following file open methods is deprecated. [Feature #19630]
    • Kernel#open
    • URI.open
    • IO.binread
    • IO.foreach
    • IO.readlines
    • IO.read
    • IO.write

Kernel#open などには open("| コマンド名") でそのコマンドを起動し、標準出力を読み取るという機能があるのですが、これが非推奨になりました。VERBOSEモードでは警告が出ます。

$ ruby -we 'open("| ls")'
-e:1: warning: Calling Kernel#open with a leading '|' is deprecated and will be removed in Ruby 4.0; use IO.popen instead

Perl から引き継いだ伝統的な機能だったのですが、活用状況や認知度のわりに、不用意に open を使って脆弱性とされることが多すぎるというのが理由でした。 とはいえ、実際にこの機能が削除されるのは Ruby 4.0 の予定なので、当面は各自ご注意ください。先頭に File. を付けて File.open("| ls")とすれば、コマンド起動はしないので、そのように修正するといいでしょう。コマンド起動を意図している場合は、IO.popen などを使って書き換えてください。

(mame)

lambda メソッドの lambda{ ... } 以外の利用はたいてい例外発生するようになった

  • When given a non-lambda, non-literal block, Kernel#lambda with now raises ArgumentError instead of returning it unmodified. These usages have been issuing warnings under the Warning[:deprecated] category since Ruby 3.0.0. [Feature #19777]

lambda(&b) に、b = proc{} のように作った Proc オブジェクトを渡すと、例外が出るようになりました。

$ ruby -e 'lambda(&Proc.new{})'
-e:1:in `lambda': the lambda method requires a literal block (ArgumentError)

lambda(&Proc.new{})
       ^^^^^^^^^^^
        from -e:1:in `<main>'

また、public_send 経由で lambda を呼ぶ場合や super 経由でブロックを渡すような場合など、つまり lambda{...} のように書かないコードもエラーになるようになりました。

$ ruby -e 'Kernel.public_send(:lambda) { 42 }'
-e:1:in `lambda': the lambda method requires a literal block (ArgumentError)

Kernel.public_send(:lambda) { 42 }
                   ^^^^^^^
        from -e:1:in `public_send'
        from -e:1:in `<main>'
$ ruby -e '
class C
  def lambda(&b)
    super
  end
end

C.new.lambda{}
'
-e:4:in `lambda': the lambda method requires a literal block (ArgumentError)
        from -e:4:in `lambda'
        from -e:8:in `<main>'

難しいことはおいといて、lambda な Proc オブジェクトを作るには、-> {...}lambda{ ... } を使いましょう、ということです。もともと、Ruby 3.0 から deprecation category の警告として出てはいたのですが、それをエラーにした、という変更になりました。

(ko1)

RUBY_GC_HEAP_INIT_SLOTS が無視されるので、別の環境変数使ってください

  • The RUBY_GC_HEAP_INIT_SLOTS environment variable has been deprecated and removed. Environment variables RUBY_GC_HEAP_%d_INIT_SLOTS should be used instead. [Feature #19785]

プログラム起動時にどれくらいヒープを用意しておくか、というヒントを渡す RUBY_GC_HEAP_INIT_SLOTS が単に無視されるようになりました。これからは、RUBY_GC_HEAP_%d_INIT_SLOTS を使ってチューニングしてください、ということです。

と、言われても、実際どうチューニングするかは難しい問題です。たいていはプログラムの定常状態(プログラムが実際に走っている様子)を見て、その定常状態でどれくらいのオブジェクトが生きているか、例えば1万個生きているようなら、1万個+αのオブジェクトが格納できるようにしておこう(あとは載せてるメモリとの兼ね合い)、みたいなチューニングができます。

Ruby 3.1 までは、ヒープの種類が1種類だったので、GC.stat をじっと眺めることでそういうことができたのですが、Ruby 3.2 から VWA によってオブジェクトのサイズごとのヒープができたので、GC.stat_heap をじっと眺めることでチューニングしましょう、ということになります。

ちなみに起動時だとこんな感じ。

$ ruby -e 'pp GC.stat_heap'
{0=>
  {:slot_size=>40,
   :heap_allocatable_pages=>0,
   :heap_eden_pages=>10,
   :heap_eden_slots=>16373,
   :heap_tomb_pages=>0,
   :heap_tomb_slots=>0,
   :total_allocated_pages=>10,
   :total_freed_pages=>0,
   :force_major_gc_count=>1,
   :force_incremental_marking_finish_count=>0,
   :total_allocated_objects=>41462,
   :total_freed_objects=>27029},
 1=>
  {:slot_size=>80,
   :heap_allocatable_pages=>4,
   :heap_eden_pages=>9,
   :heap_eden_slots=>7369,
   :heap_tomb_pages=>0,
   :heap_tomb_slots=>0,
   :total_allocated_pages=>9,
   :total_freed_pages=>0,
   :force_major_gc_count=>0,
   :force_incremental_marking_finish_count=>0,
   :total_allocated_objects=>17488,
   :total_freed_objects=>13366},
 2=>
  {:slot_size=>160,
   :heap_allocatable_pages=>18,
   :heap_eden_pages=>7,
   :heap_eden_slots=>2860,
   :heap_tomb_pages=>0,
   :heap_tomb_slots=>0,
   :total_allocated_pages=>7,
   :total_freed_pages=>0,
   :force_major_gc_count=>0,
   :force_incremental_marking_finish_count=>0,
   :total_allocated_objects=>5693,
   :total_freed_objects=>4148},
 3=>
  {:slot_size=>320,
   :heap_allocatable_pages=>49,
   :heap_eden_pages=>1,
   :heap_eden_slots=>204,
   :heap_tomb_pages=>0,
   :heap_tomb_slots=>0,
   :total_allocated_pages=>1,
   :total_freed_pages=>0,
   :force_major_gc_count=>0,
   :force_incremental_marking_finish_count=>0,
   :total_allocated_objects=>153,
   :total_freed_objects=>137},
 4=>
  {:slot_size=>640,
   :heap_allocatable_pages=>99,
   :heap_eden_pages=>1,
   :heap_eden_slots=>102,
   :heap_tomb_pages=>0,
   :heap_tomb_slots=>0,
   :total_allocated_pages=>1,
   :total_freed_pages=>0,
   :force_major_gc_count=>0,
   :force_incremental_marking_finish_count=>0,
   :total_allocated_objects=>121,
   :total_freed_objects=>46}}

%d のところは 40 bytes のページには 0、... のように上記のハッシュの key の部分を指定します。試しに、40 bytes のオブジェクトを管理するヒープを、あらかじめたくさん用意するようにしてみましょう。

$ ruby -ve 'pp GC.stat_heap[0][:heap_allocatable_pages]'
ruby 3.3.0dev (2023-12-16T21:45:33Z master d7d10f3ee8) [x86_64-linux]
0

$ RUBY_GC_HEAP_0_INIT_SLOTS=1000000 ruby -ve 'pp GC.stat_heap[0][:heap_allocatable_pages]'
ruby 3.3.0dev (2023-12-16T21:45:33Z master d7d10f3ee8) [x86_64-linux]
RUBY_GC_HEAP_0_INIT_SLOTS=1000000 (default value: 10000)
593

こんな感じで heap_allocatable_pages の値が増えていることが確認できました。

パフォーマンスチューニングはやり始めるときりがないのですが、やるときは性能改善をきちんと確認しておくといいと思います。

(ko1)

無引数 it が警告されるようになった

  • it calls without arguments in a block with no ordinary parameters are deprecated. it will be a reference to the first block parameter in Ruby 3.4. [Feature #18980]

Ruby 3.4 では、ブロック引数に it が導入される見込みになりました。次のようなコードが書けるようになる予定です。

ary = [2, 4, 6]

# Ruby 3.4 の予定!!!
ary.all? { it.even? }  #=> true

ただ、it はキーワードではなかったので、メソッド名に使われている可能性があり、非互換です。そのため Ruby 3.3 ではまず、Ruby 3.4 で意味が変わるである箇所に警告が出るようになりました。つまり地上げですね。

# Ruby 3.3
ary.map { it }
#=> warning: `it` calls without arguments will refer to the first block param in Ruby 3.4; use it() or self.it

Ruby 3.4、早くもわくわくしてきますね。

ちなみに、意味が変わる予定なのは「仮引数が省略されたブロックの内側になる、レシーバも引数も括弧もない it の呼び出し」だけです。また、ローカル変数 it は定義できる予定です。

[1, 2, 3].map { it.to_s }    # これは意味が変わる予定

[1, 2, 3].map { self.it }    # これはレシーバがあるのでメソッド呼び出しのまま
[1, 2, 3].map { it() }       # これは括弧があるのでメソッド呼び出しのまま
[1, 2, 3].map { it "Y" }     # これは引数があるのでメソッド呼び出しのまま

[1, 2, 3].map {|n| it }      # これはブロックが仮引数を明示しているので、メソッド呼び出しのまま
def foo
  it                         # これはブロックの外にあるので、メソッド呼び出しのまま
end

[1, 2, 3].map do
  it         # これは意味が変わる予定だが
  it = "foo" # ローカル変数 it の定義より字面上あとにある場合はローカル変数になる
  it         # つまりこれはローカル変数の読み出し
end

いやあ、違う意味でもわくわくしますね。

なお、rspec がメソッド it を使っていますが、その it はふつう引数付きで呼び出すので、互換性の問題はない見込みです。というか、主に rspec の互換性を保つためにだいぶややこしい導入の形になりました。

(mame)

NoMethodError の表示形式が変わった

  • Error message for NoMethodError have changed to not use the target object's #inspect for efficiency, and says "instance of ClassName" instead. [Feature #18285]

レシーバの中身が inspect されなくなりました。

Ruby 3.2 では次のようなメッセージでした。

# Ruby 3.2
[1, 2, 3, 4, 5].foobar
#=> undefined method `foobar' for [1, 2, 3, 4, 5]:Array (NoMethodError)

これが Ruby 3.3 では次のようになります。

# Ruby 3.3
[1, 2, 3, 4, 5].foobar
#=> undefined method `foobar' for an instance of Array (NoMethodError)

違いはメッセージの最後のところです。Ruby 3.2 では for [1, 2, 3, 4, 5]:Array というように、#inspect を使ってレシーバの中身を表示していたのが、Ruby 3.3 からは for an instance of Array のように、クラス名だけ表示されます。

変更のきっかけは、Datadogの人から「NoMethodErrorのエラー表示が遅い」という報告を受けたことです。普通の例外と比べても遅いようですが、一部のクラス(たとえばRailsのコントローラ)では、時間のかかる #inspect が実装されているなどで、より一層遅かったとのことです。

そこで、#inspect を使わず、クラス名だけを表示することになりました。

今まで表示されていた情報が減ることに不安を覚える声もありますが、1 年弱開発版 Ruby で暮らしていた個人的感想としては、特段こまったことは 1 度もありませんでした。NoMethodError のデバッグに必要なのはレシーバ自体の情報ではなく、クラスの情報なのでしょう(メタプログラミングを乱用などしていない限り……)。

#inspect が長い文字列を返す場合に、エラー表示で刺さったように遅くなったり、ターミナルが流れてしまったりしていたのが解消したので、複雑なデータ構造を扱う場合などで快適になりました。

([1] * 1000).foobar

# Ruby 3.2 の出力はとても長い
#=> undefined method `foobar' for [1, 1, 1, 1, 1, 1, 1, 1,
#   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
#   , 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
#   ...
#    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
#   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
#   , 1, 1, 1, 1, 1, 1, 1]:Array (NoMethodError)
    
# Ruby 3.3 の出力はすっきり
#=> undefined method `foobar' for an instance of Array (NoMethodError)

余談ですが、Ruby 2.7 以前は #inspect を呼ぶものの、長さが 60 文字を超えるときは undefined method `foo' for #<Array:0x0000564f05c8a870> みたいな表記にしていたので、ターミナルが流れる問題はありませんでした。Ruby 3.0 からこの省略がなされなくなったので、問題が顕在化しました。

(mame)

ブロック中での紛らわしい匿名引数の移譲が禁止された

  • Now anonymous parameters forwarding is disallowed inside a block that uses anonymous parameters. [Feature #19370]

ブロックの中で匿名のメソッド引数を移譲することが禁止になりました。と言われてもなんじゃらほい、って感じだと思うので例で示します。

Rubyのブロックは 1.times{|*|} のように、ブロックに何個引数を受け取っても無視する、という記法がありました。そして、Ruby 3.2 から、メソッドの匿名引数を別のメソッドに移譲できるようになりました(def f(*) = g(*)def f(*a) = g(*a) と同じ)。これが合体すると、ちょっと混乱するような例が書けます。

def f(*) = ->(*) { g(*) }

# Ruby 3.2 ではこの意味になる
def f(*a) = ->(*b) { g(*a) }

そこで、このような混乱を生むようなケースはそもそも SyntaxError にしてしまおう、ということになりました。Ruby 3.2 の新文法なので、あまり使われていないと思いますが、今後は禁止されるのでご注意ください。

def (*) = ->(*) { g(*) }
#=> anonymous rest parameter is also used within block (SyntaxError)

余談ですが、もともとは「ブロックの引数も移譲するべき?」というところから議論が始まったのですが、あまりに見た目が紛らわしいので禁止してしまおう、となりました。今後、もしかしたらブロックの引数も移譲しとこうか、となるかもしれません。

さらに余談ですが、NEWS エントリでは「inside a block that uses anonymous parameters」と、匿名ブロック引数をうけるブロックの時に禁止されます、とありますが、ブロック一般で利用できないようになっています。

def f(*) = 1.times{ g(*) }
#=> anonymous rest parameter is also used within block (SyntaxError)

リリース直前の変更だったので、ちゃんと確認できてなかったのですが、ちょっと禁止し過ぎているような気がするので、ひょっこり Ruby 3.3.1 で匿名ブロック引数をうけるブロックでだけエラーになるように修正されるかもしれません(追記: やっぱり修正されそう: [Feature #19370] Blocks without anonymous parameters should not affect by nobu · Pull Request #9350 · ruby/ruby )。

(ko1)

Stdlib の非互換

  • racc is promoted to bundled gems.
    • You need to add racc to your Gemfile if you use racc under bundler environment.
  • ext/readline is retired
    • We have reline that is pure Ruby implementation compatible with ext/readline API. We rely on reline in the future. If you need to use ext/readline, you can install ext/readline via rubygems.org with gem install readline-ext.
    • We no longer need to install libraries like libreadline or libedit.

racc が default gem から bundled gem に変わりました。使用している人は、適宜 Gemfilegemspec に依存を明示するようにしてください。

また、拡張ライブラリの readline ライブラリが削除されました。require "readline" すると、default gem である reline の readline の互換レイヤがロードされます。もし従来の拡張ライブラリの readline が必要になった場合は、gem install readline-ext などでインストールしてください。

(mame)

C API のアップデート

C API に下記のような変更が入りました。

  • rb_postponed_job updates
    • New APIs and deprecated APIs (see comments for details)
      • added: rb_postponed_job_preregister()
      • added: rb_postponed_job_trigger()
      • deprecated: rb_postponed_job_register() (and semantic change. see below)
      • deprecated: rb_postponed_job_register_one()
    • The postponed job APIs have been changed to address some rare crashes. To solve the issue, we introduced new two APIs and deprecated current APIs. The semantics of these functions have also changed slightly; rb_postponed_job_register now behaves like the once variant in that multiple calls with the same func might be coalesced into a single execution of the func [Feature #20057]

C extension を書いているとき、何か Ruby に関する処理をしたい場合、「安全な場所」まで処理を遅延させたい、ということが時々あります。ここでいう「安全な場所」というのは、任意の Ruby コードを実行してもよい場所という意味で、例えばバイトコード1命令が終了したタイミングだったり、メソッドが終了したタイミングだったりです。ここでいう処理とは、例えばファイナライザを実行させたり、trap{...} メソッドで登録したシグナルハンドラの処理を実行したり、みたいなやつです。

で、Ruby では以前から rb_postponed_job_register() という C API を用意して、この「安全なタイミングで処理を実行して」ということを登録しておく(関数ポインタを登録して、あとで呼び出してもらう)ことができたのですが、この API を別のネイティブスレッド(Ruby とは関係ないスレッド)から呼んだり、(Ruby の trap ではないネイティブの)シグナルハンドラで実行したり、という、結構きわどいタイミングで呼ぶとまれにエラーになる、という報告を受けていました。そんなところで呼ぶなよって感じですが、なんとなく呼べるような雰囲気になっていたようです。多分、プロファイラとかそういうのが使うんでしょう。

そこで、えいやと実装を刷新して、どんなタイミングで呼ばれても大丈夫になるようにしました。ついでに、新 C API を用意して、既存のものを deprecated にしました。新 API を使うのがおすすめです(が、多分この API を使いたい人はあまりいないでしょう)。

この刷新では、実装を簡単にするために、仕様の縮小が行われています。具体的には、rb_postponed_job_register()rb_postponed_job_register_one() の意味(実際にはちょっと違うけど)になるようになっています。実際に公開されている gem を探したところ「多分大丈夫だろう」ということで、この判断を行いました。もし影響がありそうだったらはやめにご指摘ください。

  • Some updates for internal thread event hook APIs
    • rb_internal_thread_event_data_t with a target Ruby thread (VALUE) and callback functions (rb_internal_thread_event_callback) receive it. https://github.com/ruby/ruby/pull/8885
    • The following functions are introduced to manipulate Ruby thread local data from internal thread event hook APIs (they are introduced since Ruby 3.2). https://github.com/ruby/ruby/pull/8936
      • rb_internal_thread_specific_key_create()
      • rb_internal_thread_specific_get()
      • rb_internal_thread_specific_set()

Rubyには、extension からだけ使える、スレッドの状態を追うためのフック機能があります。これについて、次の拡張が行われました。

  • フックでイベント対象となる thread オブジェクトを取得できるようになりました。
  • thread オブジェクトに紐づいた thread specifc データを設定・取得できるようになりました。

これらは、主にスレッドプロファイラ向けの拡張になります。

  • rb_profile_thread_frames() is introduced to get a frames from a specific thread. [Feature #10602]

こちらもプロファイラ向けの機能です。これまでカレントスレッドのフレーム情報を取得する rb_profile_frames() 関数しかなかったのですが、任意のスレッドのフレーム情報を取得する関数が導入されました。

rb_profile_frames() 関数導入時に別スレッドどうする、って話もしたんですが、とりあえずカレントだけとれりゃいいだろ、ってことで入ったのですが、やっぱり別スレッドも欲しいねってことがあったようです。

新しい Data クラスを定義する関数が導入されました。

  • rb_ext_resolve_symbol() is introduced to search a function from extension libraries. [Feature #20005]

別の C extension で公開している関数ポインタを名前指定して取得する関数が導入されました。

  • IO related updates:
    • The details of rb_io_t will be hidden and deprecated attributes are added for each members. [Feature #19057]
    • rb_io_path(VALUE io) is introduced to get a path of io.
    • rb_io_closed_p(VALUE io) to get opening or closing of io.
    • rb_io_mode(VALUE io) to get the mode of io.
    • rb_io_open_descriptor() is introduced to make an IO object from a file descriptor.

rb_io_t というのが公開ヘッダに記述されているんですが、この実装を隠そうと思っています。というのも、公開していると変更しづらいからですね。そのために、直接アクセスすると deprecate warning が出るようにしたり、アクセサ関数を用意したりしています。

多分、rb_io_t::fd へアクセスがよく利用されると思うんですが、これからは rb_io_descriptor() を利用するなど、構造体に直接アクセスしないようにしてください。

(ko1)

実装の改善

内部実装の改善です。基本的にユーザから直接見えるものではありませんが、せっかくなので紹介します。

パーサ生成器が Bison から Lrama に変更された

  • Replace Bison with Lrama LALR parser generator. No need to install Bison to build Ruby from source code anymore. We will no longer suffer bison compatibility issues and we can use new features by just implementing it to Lrama. Feature #19637
    • See The future vision of Ruby Parser for detail.
    • Lrama internal parser is a LR parser generated by Racc for maintainability.
    • Parameterizing Rules (?, *, +) are supported, it will be used in Ruby parse.y.

現在の Ruby のパーサは parse.y というソースファイルで記述されています。Ruby では parse.y を C 言語ソースコード parse.c に変換し、ビルドしています。この変換をしてくれるツールをパーサ生成器と言います。

この変更は、Ruby で使用するパーサ生成器を、Bison から Lrama に置き換えたという話です。

先に明確にしておくと、Ruby の配布パッケージには生成済みの parse.c が含まれているため、ユーザが直接意識する必要はありません。

なぜ Lrama に置き換えたかというと、Ruby 開発者にとって Bison が辛かったからです。古いバージョンの Bison を使う開発者もいるためなかなか新しい機能が使えないとか、Bison に機能が不足していてできないことがある(しょうがないので出力されたファイルを sed で書き換えていた)とか。

Lrama は Ruby のために新しく作られた、Ruby で書かれたパーサ生成器です。Ruby のリポジトリに含めてしまえるのでバージョンの問題はありません。また、困ったことがあったら Lrama を直すことができます。というパワーによる解決で、Bison のつらみから開放されることになりました。

当初は parse.y をそのまま動かす目標で開発されていましたが、現在は Lrama に(Bison を超えた)新機能を入れることで、parse.y をよりシンプルに、メンテナンス性向上させていくことを目指しています。Ruby のパーサが Prism に置き換えられるか、メンテナンス性の上がった parse.y になって維持されるかは、今後の開発動向次第です。

おそらく Ruby は世界一文法が複雑な言語のひとつだと思っているのですが、そんな言語のパーサ実装に首を突っ込む人が多くてすごいですね。

(mame)

defined?(@ivar) が最適化された

  • defined?(@ivar) is optimized with Object Shapes.

defined?(@ivar) という、@ivar の存在を確認する式が Object Shapes を使ってうまいこと高速化されるようになりました。

(ko1)

GC

  • Major performance improvements over Ruby 3.2
    • Young objects referenced by old objects are no longer immediately promoted to the old generation. This significantly reduces the frequency of major GC collections. [Feature #19678]
    • A new REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO tuning variable was introduced to control the number of unprotected objects cause a major GC collection to trigger. The default is set to 0.01 (1%). This significantly reduces the frequency of major GC collection. [Feature #19571]
    • Write Barriers were implemented for many core types that were missing them, notably Time, Enumerator, MatchData, Method, File::Stat, BigDecimal and several others. This significantly reduces minor GC collection time and major GC collection frequency.
    • Most core classes are now using Variable Width Allocation, notably Hash, Time, Thread::Backtrace, Thread::Backtrace::Location, File::Stat, Method. This makes these classes faster to allocate and free, use less memory and reduce heap fragmentation.

GC がたくさん改善されました。

  • 古い世代のオブジェクトから参照された新しい世代のオブジェクトを、これまではすぐ古い世代にしていたのを、その世代変更を少し遅延することによって、世代別GCにおいて時間のかかるフルGC(major GC)の頻度が下がりました。
  • RUBY_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO という環境変数で、WB unprotected object の数に起因する major GC の回数をチューニングできるようになりました。具体的には、古い世代のオブジェクトからの割合も考慮に入れられるようになりました。デフォルト値は 0.01(1%)だそうです。この名前、WB unprotected objects の数に対する割合ではなく、古い世代のオブジェクトの数への割合なので、ちょっと微妙だと思うんだよな。
  • 世代別GCで必要となるライトバリア(WB)を、複数のクラスに導入しました。WB がない場合は、時間がかかるけどちゃんとうごく、というアルゴリズムで、頑張って WB 入れれば速くなるぞ、みんなで移行しようね、という方針だったんですが、その移行処置を進めたということですね。Timeはよく使うので対応したかったんですが、よくわからなくて放置していたので対応されてよかったです。
  • いろんなクラスで VWA (Variable Width Allocation) の対応が進みました。

私はあんまり実装を追えなくなっているんですが、だいたい Shopify のアプリで実際に効果がでるという報告が出ているので、多くの Rails アプリで効果が期待できるんじゃないかと思います。

(ko1)

YJIT

  • Major performance improvements over Ruby 3.2
    • Support for splat and rest arguments has been improved.
    • Registers are allocated for stack operations of the virtual machine.
    • More calls with optional arguments are compiled.
    • Exception handlers are also compiled.
    • Instance variables no longer exit to the interpreter with megamorphic object shapes.
    • Unsupported call types no longer exit to the interpreter.
    • Integer#!=, String#!=, Kernel#block_given?, Kernel#is_a?, Kernel#instance_of?, Module#=== are specially optimized.
    • Now more than 3x faster than the interpreter on optcarrot!
  • Significantly improved memory usage over Ruby 3.2
    • Metadata for compiled code uses a lot less memory.
    • Generate more compact code on ARM64
  • Compilation speed is now slightly faster than 3.2.
  • Add RubyVM::YJIT.enable that can enable YJIT later
    • You can start YJIT without modifying command-line arguments or environment variables.
    • This can also be used to enable YJIT only once your application is done booting. --yjit-disable can be used if you want to use other YJIT options while disabling YJIT at boot.
  • Code GC now disabled by default, with --yjit-exec-mem-size treated as a hard limit
    • Can produce better copy-on-write behavior on forking web servers such as unicorn
    • Use the --yjit-code-gc option to automatically run code GC when YJIT reaches the size limit
  • ratio_in_yjit stat produced by --yjit-stats is now available in release builds, a special stats or dev build is no longer required to access most stats.
  • Exit tracing option now supports sampling
    • --trace-exits-sample-rate=N
  • More thorough testing and multiple bug fixes
  • --yjit-stats=quiet is added to avoid printing stats on exit.
  • --yjit-perf is added to facilitate profiling with Linux perf.

YJITに様々な改良が導入されたとのことです。中味はきっとどこかで解説が出ると思うのですが、ユーザーから見える変更として:

(ko1)

MJIT

  • MJIT is removed.
    • --disable-jit-support is removed. Consider using --disable-yjit --disable-rjit instead.

MJIT が完全に削除されました。また、configure オプションである --disable-jit-support がなくなりました。明確に --disable-yjit --disable-rjit を指定するように、とのことです。

(ko1)

RJIT

  • Introduced a pure-Ruby JIT compiler RJIT.
    • RJIT supports only x86_64 architecture on Unix platforms.
    • Unlike MJIT, it doesn't require a C compiler at runtime.
  • RJIT exists only for experimental purposes.
    • You should keep using YJIT in production.

RubyでJITを記述する基盤であるRJITが導入されました。実験的なJITを実装しやすくするための導入だそうです。

(ko1)

M:N Thread scheduler

  • M:N Thread scheduler is introduced. [Feature #19842]
    • Background: Ruby 1.8 and before, M:1 thread scheduler (M Ruby threads with 1 native thread. Called as User level threads or Green threads) is used. Ruby 1.9 and later, 1:1 thread scheduler (1 Ruby thread with 1 native thread). M:1 threads takes lower resources compare with 1:1 threads because it needs only 1 native threads. However it is difficult to support context switching for all of blocking operation so 1:1 threads are employed from Ruby 1.9. M:N thread scheduler uses N native threads for M Ruby threads (N is small number in general). It doesn't need same number of native threads as Ruby threads (similar to the M:1 thread scheduler). Also our M:N threads supports blocking operations well same as 1:1 threads. See the ticket for more details. Our M:N thread scheduler refers on the goroutine scheduler in the Go language.
    • In a ractor, only 1 thread can run in a same time because of implementation. Therefore, applications that use only one Ractor (most applications) M:N thread scheduler works as M:1 thread scheduler with further extension from Ruby 1.8.
    • M:N thread scheduler can introduce incompatibility for C-extensions, so it is disabled by default on the main Ractors. RUBY_MN_THREADS=1 environment variable will enable it. On non-main Ractors, M:N thread scheduler is enabled (and can not disable it now).
    • N (the number of native threads) can be specified with RUBY_MAX_CPU environment variable. The default is 8. Note that more than N native threads are used to support many kind of blocking operations.

1:1 スレッドではなく、M:N スレッド(N 個のネイティブスレッドで M 個の Ruby スレッドを管理する Ruby のスレッド実装。一般的に M>>N)が導入されました。

細かい経緯や狙いなどはこちら:Rubyの並列並行処理のこれまでとこれから - クックパッド開発者ブログ

あまり実装はこなれていないので、とりあえず production ではまだ厳しそうな気がします。そのため、デフォルトでは有効になっていません。RUBY_MN_THREADS=1 と環境変数を指定することで、main Ractor 内のスレッドスケジューリングを M:N スレッド実装を使うようにすることができます。main Ractor 以外では、M:N スレッド実装を使うことになります。

当初 Linux にだけ対応してリリースしようと思っていたんですが、最後のさいごで macOS や FreeBSD などにも対応しました。有効になると、バージョン表記に +MN というのが付きます。対応していないプラットフォームでは、単に環境変数が無視されます。

$ RUBY_MN_THREADS=1 ruby -v
ruby 3.3.0dev (2023-12-24T14:03:55Z master 42442ed789) +MN [x86_64-linux]

ちなみに、用意するネイティブスレッドの数の上限は RUBY_MAX_CPU=4 みたいに指定します。デフォルトは 8 です。

この変更のためにスレッド実装をまるっと書き換えたので、「M:N スレッドを有効にしないときの不具合」を見つけたら、ぜひご報告ください。M:N スレッドを有効にしたときの体験談も頂けると嬉しいです。実装がこなれていないと前述しましたが、まだバグがありそうなこともあるのですが、性能チューニングが全然できていないので、その辺をさらに進めていきたいと思っています。

(ko1)

終わりに

Ruby 3.3 の新機能や改善を紹介してきました。ここで紹介した以外でも、バグの修正や細かな改善が行われています。お手元の Ruby アプリケーションでご確認いただければと思います。

Ruby 3.3 は目立つ新機能はないのですが、たくさんの実装の改善が行われています。また、Prism や Lrama が導入されたり、it という新しい文法を導入する準備がされるなど、今後の Ruby の展開を楽しみにさせるものだと思います。ぜひ、お手元にセットアップして新しい Ruby を楽しんでください。

Enjoy Ruby programming!

(ko1/mame)