テクノロジー部門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 2.6 NEWS ファイル - クックパッド開発者ブログ
- プロと読み解くRuby 2.7 NEWS - クックパッド開発者ブログ
- プロと読み解く Ruby 3.0 NEWS - クックパッド開発者ブログ
- プロと読み解く Ruby 3.1 NEWS - クックパッド開発者ブログ
- プロと読み解く Ruby 3.2 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
orWarning[: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 theBUG 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 raisesArgumentError
for unknown directives. [Bug #19150]
Array#pack
や String#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 providedDir
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
が導入された
- Introduce
Fiber#kill
. [Bug #595]
Fiber#kill
が導入されました。Thread#kill
に似ているのですが、次のような操作をします。
- その Fiber に resume する。
- resume 先で、絶対に rescue できない例外(ここでは E)を raise する(これは、インタプリタ終了時に各スレッドで発生する例外とだいたい同じです)。
- resume 先で例外 E によってトップレベルまで巻き戻ったら
Fiber.yield
でFiber#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_captures
に symbolize_names
キーワードが指定できるようになった
MatchData#named_captures
now accepts optionalsymbolize_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#dup
と Proc#clone
がちゃんと initialize フックを呼ぶようになった
- Now
Proc#dup
andProc#clone
call#initialize_dup
and#initialize_clone
hooks respectively. [Feature #19362]
Proc#dup
と Proc#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#&
andProcess::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 raisesTypeError
. [Bug #17146]Thread::SizedQueue#freeze
now raisesTypeError
. [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)
TracePoint
に rescue
イベントが追加された
TracePoint
- TracePoint supports
rescue
event. When the raised exception was rescued, the TracePoint will fire the hook.rescue
event only supports Ruby-levelrescue
. [Feature #19572]
- TracePoint supports
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 withDISABLE_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
andSocket#recv_nonblock
returnsnil
instead of an empty string on closed connections.Socket#recvmsg
andSocket#recvmsg_nonblock
returnsnil
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 に聞いてみました。「pitchfork で pipe
から 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 optionalchars
keyword argument. [Feature #18183]
端的に言えば、SecureRandom.alphanumeric
で使用する文字種を指定できるようになりました。
require "securerandom" SecureRandom.alphanumeric(10, chars: ["A", "B", "C"]) #=> "ACBAABBCBC"
Random::Formatter
は SecureRandom
だけでなく 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 objectPrism.parse_comments(source)
which returns the commentsPrism.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
orRUBYOPT="--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 がアドベントカレンダーを書いていたので、興味ある方はご参照ください。
(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 variablesRUBY_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 yourGemfile
if you useracc
under bundler environment.
- You need to add
ext/readline
is retired- We have
reline
that is pure Ruby implementation compatible withext/readline
API. We rely onreline
in the future. If you need to useext/readline
, you can installext/readline
via rubygems.org withgem install readline-ext
. - We no longer need to install libraries like
libreadline
orlibedit
.
- We have
racc
が default gem から bundled gem に変わりました。使用している人は、適宜 Gemfile
や gemspec
に依存を明示するようにしてください。
また、拡張ライブラリの 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()
- added:
- 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 theonce
variant in that multiple calls with the samefunc
might be coalesced into a single execution of thefunc
[Feature #20057]
- New APIs and deprecated APIs (see comments for details)
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()
関数導入時に別スレッドどうする、って話もしたんですが、とりあえずカレントだけとれりゃいいだろ、ってことで入ったのですが、やっぱり別スレッドも欲しいねってことがあったようです。
rb_data_define()
is introduced to defineData
. [Feature #19757]
新しい 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 ofio
.rb_io_closed_p(VALUE io)
to get opening or closing ofio
.rb_io_mode(VALUE io)
to get the mode ofio
.rb_io_open_descriptor()
is introduced to make an IO object from a file descriptor.
- The details of
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 to0.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
- Can produce better copy-on-write behavior on forking web servers such as
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に様々な改良が導入されたとのことです。中味はきっとどこかで解説が出ると思うのですが、ユーザーから見える変更として:
RubyVM::YJIT.enable
で YJIT を起動時ではなく、任意のタイミングで有効にすることができるようになりました。Rails だともう使ってるようですね(Enable YJIT by default if running Ruby 3.3+ by byroot · Pull Request #49947 · rails/rails )- コードGCがデフォルトで無効になりました。有効にするには
--yjit-code-gc
を指定するとのことです。
(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 withRUBY_MAX_CPU
environment variable. The default is 8. Note that more thanN
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)