Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

RubyのRactorを解き放つ(1)object_idの改修(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

RubyのRactorを解き放つ(1)object_idの改修(翻訳)

Ractorに関する過去記事では、アプリケーション全体を1つのRactor内で実行できる可能性は低いと私が考えている理由と、それでもRactorは、状況によっては、CPUバウンドの処理をメインスレッドから追い出して一部のパラレルアルゴリズムを有効にするうえで非常に有効であると思われる理由について説明しました。

しかし同記事で既に述べたように、残念ながら現時点のRactorではまだ実現できません。Ractorにはインタプリタをクラッシュさせる既知のバグが多数残っています。また、Ractor同士はパラレル実行が想定されているにもかかわらず、Ruby VMには単一の(真の意味での)グローバルロックがまだ存在していて、Ractorがある種の操作を実行する場合はそのロックを取得する必要があるため、同等のシングルスレッドのコードよりもパフォーマンスが低下しがちです。

しかし状況は急速に進みつつあります。その後、まさにRactorのこうした問題を修正する目的で、既知のバグへの対処と、既存の競合ポイントの排除・削減を行うためのチームが編成されました。

既存の競合ポイントとして私が例に取り上げたのは、fstring_tableでした。これは簡単に言うと、文字列の重複を排除するための一種の巨大な内部ハッシュテーブルであり、RubyはHashで文字列キーを使うたびにこの重複排除を実行しています。このテーブルに別のRactorが新しいエントリを挿入している最中にこのテーブルを参照するとクラッシュする(あるいはもっと悪い結果になる)可能性があります。そのため、先週までのRubyは、このテーブルにアクセスするときに残りのVMロックを必ず取得しなければならなかったのです。

しかし最近になってJohn Hawthornが、これをロックフリーのハッシュセットに置き換えてくれた(#21268)おかげで、この競合ポイントが解消されました。過去記事で紹介したJSONベンチマークを最新のRuby masterブランチで再実行すると、Ractorバージョンはシングルスレッドバージョンより3倍遅くなるどころか、むしろ2倍速くなりました。

ただしこの機能は、まだ完璧ではありません。このベンチマークでは5つのRactorを使っているので、本来なら理想的な状況においてシングルスレッドのほぼ5倍高速になるはずです。そのため、残りの競合ポイントを排除・削減するための作業がまだまだ必要です。

読者の皆さんにとって、おそらく思いもよらないであろう競合ポイントの1つに、Rubyの#object_idメソッドがあります。私はRubyKaigiから戻る旅路の途中で、この問題に手を付け始めていました。

しかし、私が#object_idをどうするつもりかを説明する前に、この#object_idメソッドがなぜ論争の的になったのかについて話しておきたいと思います。

🔗 #object_idの歴史を手短に振り返る

Ruby 2.6までの#object_idの実装は、以下のように実に簡素なものでした。

VALUE
rb_obj_id(VALUE obj)
{
    if (STATIC_SYM_P(obj)) {
        return (SYM2ID(obj) * sizeof(RVALUE) + (4 << 2)) | FIXNUM_FLAG;
    }
    else if (FLONUM_P(obj)) {
      return LL2NUM((SIGNED_VALUE)obj);
    }
    else if (SPECIAL_CONST_P(obj)) {
      return LONG2NUM((SIGNED_VALUE)obj);
    }
    return LL2NUM((SIGNED_VALUE)(obj) / 2);
}

C言語なので初心者には少々難しいかもしれませんが、動作を手短に説明するとこうです。オブジェクトがヒープにアロケーションされる一般的なケースでは、そのオブジェクトのobject_idは、そのオブジェクトが保存されているメモリアドレスを2で割った値になります。つまり、object_idはある意味、そのオブジェクトを指す実際のポインタを返すのに使われていたとも言えます。

このおかげで、object_idのあまり知られていない相方であるObjectSpace._id2ref(訳注: オブジェクトidを渡すとオブジェクトの参照を返すメソッド)を実装しやすくなりました(object_idを2倍するだけで、対応するオブジェクトへのポインタを得られます)。

s = "I am a string"
ObjectSpace._id2ref(s.object_id).equal?(s) # => true

しかしこの実装には1つ大きな問題がありました。その問題とは、Rubyのヒープが標準的なサイズのスロットで構成されていることです。
オブジェクトへの参照が存在しなくなると、GCがオブジェクトスロットを回収して、今後生成されるであろうオブジェクトで再利用するときに備えます。

つまり、たまたま取得したobject_idを用いてObjectSpace._id2refメソッドを実行したとしても、それによって得られるオブジェクトが本当にobject_idの取得元オブジェクトと同一であるかどうかは、実際には不確実であり、まったく別のオブジェクトを取ってきてしまう可能性もあるのです。

これは、「そのオブジェクトを既に参照したかどうか」を知るための手段としてobject_idを取得したときに、誤った結果を得る可能性があるということでもあります。

そういうわけで、2018年には#object_id_id2refの両方を非推奨化しようという機能リクエストが提出されました(#15408)。当時のMatzはRuby 2.7で_id2refを非推奨化することについては合意しましたが、#object_idを削除した場合のbreaking change(破壊的変更)が大きすぎることと、#object_idがAPIとして有用であることを指摘しました。
しかしこの指摘はどういうわけか見過ごされてしまい、_id2refは現在に至るまで公式には非推奨化されていません。私はRuby 3.5で非推奨化しておきたいと思っています(#13157)(訳注: その後マージされました)。

cvs2svnで生成された1999年のコミット(210367ec889)を私がgit blameでチェックした限りでは、そもそも_id2refがなぜ最初に追加されたのかという理由は定かではありません。しかし私は、_id2refが追加された理由は、もしかするとdrbのためだったのではないかと推測しています。drbは、現時点の標準ライブラリでこのAPIを利用している唯一の重要なユーザーですが、その状況も今や変わりつつあります(#35)。

ruby/drb - GitHub

🔗 GCコンパクション

_id2refが追加された理由はともかく、この設計上の重大な欠点は、Aaron PattersonがRuby 2.7でGCコンパクション(compaction: 圧縮)を実装するうえで障害となりました(#15626)。GCコンパクションでは、オブジェクトがスロットから別スロットへ移動する可能性があるため、その場合#object_idからオブジェクトのアドレスを取り出せなくなり、安定性を維持できなくなります。

Aaronによる対応策は、概念上はシンプルです。

module Kernel
  def object_id
    unless id = ObjectSpace::OBJ_TO_ID_TABLE[self]
      id = ObjectSpace.next_obj_id
      ObjectSpace.next_obj_id += 8
      ObjectSpace::OBJ_TO_ID_TABLE[self] = id
      ObjectSpace::ID_TO_OBJ_TABLE[id] = self
    end
    id
  end
end

module ObjectSpace
  def self._id2ref(id)
    ObjectSpace::ID_TO_OBJ_TABLE[id]
  end
end

要するに、Rubyに内部ハッシュテーブルが2つ追加されたのです。
テーブルの1つはオブジェクトをキー、IDを値として保存し、他方のテーブルは逆にIDをキー、オブジェクトを値として保存します。
オブジェクトのIDに初めてアクセスするたびに、内部カウンタをインクリメントする形で一意のIDが作成され、オブジェクトとIDの関係が2つのハッシュテーブルに保存されます。

Rubyユーザーは、object_idを次のように出力することでこの変更を手軽に確認できます。

p Object.new.object_id
p Object.new.object_id

Ruby 2.6までは、上のコードを実行すると、50666405449360のように、いかにもランダムな桁数の大きい整数値が出力されますが、Ruby 2.7以降は816といった桁数の小さい整数値が出力されます。

この変更により、従来の_id2refの問題が解決され、オブジェクトをあるアドレスから別のアドレスに移動するときのGCがIDを安定して維持できるようになりましたが、その代わりobject_idのコストは以前よりもずっと高くなってしまいました。

Rubyのハッシュテーブルの実装では、エントリごとに3つのポインタサイズの数値が格納されます。1つはキー用、1つは値用、そしてもう1つはハッシュコード用です

struct st_table_entry {
    st_hash_t hash;
    st_data_t key;
    st_data_t record;
};

すべてのobject_idが2つのハッシュテーブルに保存されることを考えると、object_id1つあたり48B(と少々)のメモリを消費することになります。こんなちっぽけな数値を保存するために、かなりのメモリを食っていることになります。

さらに、従来はアドレスを2で割るだけでobject_idを得られたのに、改修後はobject_idでハッシュ探索が必須となったため、IDを持つオブジェクトをGCが移動・解放するたびに、2つのハッシュテーブルを更新しなければならなくなりました。

念のために申し上げておくと、2つのテーブルが追加されたことで、RubyアプリケーションにおけるメモリやCPUのオーバーヘッドが現実に生じているという証拠を、私は何も持ち合わせていません。#object_idのコストが思ったよりも随分大きいと言いたかったに過ぎません。

🔗 Ractorの登場

その後Koichi SasadaがRactorを実装したときに、2つのハッシュテーブルに複数のRactorがコンカレントなアクセスを試みる可能性があったため、#object_idの周囲にロックを追加しなければならなくなり(da3438a)、その結果#object_idが競合ポイントになったのです。

module Kernel
  def object_id
    RubyVM.synchronize do
      unless id = ObjectSpace::OBJ_TO_ID_TABLE[self]
        id = ObjectSpace.next_obj_id
        ObjectSpace.next_obj_id += 8
        ObjectSpace::OBJ_TO_ID_TABLE[self] = id
        ObjectSpace::ID_TO_OBJ_TABLE[id] = self
      end
      id
    end
  end
end

module ObjectSpace
  def self._id2ref(id)
    RubyVM.synchronize do
      ObjectSpace::ID_TO_OBJ_TABLE[id]
    end
  end
end

ここまで読んでいただいた方の中には、これがそんなに重大なのかと疑問に思う人がいるかもしれません。#object_idはデバッグでたまに使われる程度で、現実のproductionコードではそれほど使われていません。そのことはほぼ事実なのですが、以下のように現実のコードで使われているのも確かです。

しかし、オブジェクトIDに依存するのは、Kernel#object_id呼び出しだけとは限りません。

たとえば、Object#hashメソッドもオブジェクトIDに依存しています。

static st_index_t
objid_hash(VALUE obj)
{
    VALUE object_id = rb_obj_id(obj);
    if (!FIXNUM_P(object_id))
        object_id = rb_big_hash(object_id);

    return (st_index_t)st_index_hash((st_index_t)NUM2LL(object_id));
}

VALUE
rb_obj_hash(VALUE obj)
{
    long hnum = any_hash(obj, objid_hash);
    return ST2FIX(hnum);
}

StringArrayなどのよく使われる値クラスは、オブジェクトIDに依存しない#hashメソッドを独自に定義していますが、それ以外の、オブジェクト同士の比較をIDベースで行うあらゆるオブジェクトは最終的にObject#hashを利用しているので、必然的にobject_idにアクセスすることになります。

たとえば、Railsのあるクラスでは、#hashメソッドを以下のように実装しています。

#  activerecord/lib/arel/nodes/delete_statement.rb
  def hash
    [self.class, @relation, @wheres, @orders, @limit, @offset, @key].hash
  end

このコードからはまったく見当もつきませんが、ここではClassオブジェクトをハッシュ化しており、このクラスはデフォルトのオブジェクトと同様にIDで添字化されます。

>> Class.new.method(:hash).owner
=> Kernel
>> Object.new.method(:hash).owner
=> Kernel

つまり上のコードは、現時点はハッシュを生成するためだけにVM全体をロックしなければならないのです。

🔗 最適化を解除する

では、オブジェクトIDにアクセスするときにいちいちVM全体をロックして同期する必要性を排除・削減するには、どうすればよいでしょうか?

最初に考えられるのは、ObjectSpace._id2refがほぼ使われておらず、間もなく非推奨化される見込みであることを前提として、必要が生じるまではid -> objectテーブルを作成・更新しないようにするという楽観的な方法です。これに該当するプログラムが極力存在していないことを願うばかりです。

module Kernel
  def object_id
    RubyVM.synchronize do
      unless id = ObjectSpace::OBJ_TO_ID_TABLE[self]
        id = ObjectSpace.next_obj_id
        ObjectSpace.next_obj_id += 8
        ObjectSpace::OBJ_TO_ID_TABLE[self] = id
        if defined?(ObjectSpace::ID_TO_OBJ_TABLE)
          ObjectSpace::ID_TO_OBJ_TABLE[id] = self
        end
      end
      id
    end
  end
end

module ObjectSpace
  def self._id2ref(id)
    RubyVM.synchronize do
      unless defined?(ObjectSpace::ID_TO_OBJ_TABLE)
        ObjectSpace::ID_TO_OBJ_TABLE = ObjectSpace::OBJ_TO_ID_TABLE.invert
      end
      ObjectSpace::ID_TO_OBJ_TABLE[id]
    end
  end
end

これでロックが削除されるわけではありませんが、ユーザーのプログラムがObjectSpace._id2refを決して呼び出さないことを前提にできれば、ロック内部の処理をいくらか削除できるため、ロックの保持期間を短縮できるはずです。
また、Ractorを使わない場合であっても、マイクロベンチマークの結果で示したように、メモリ使用量やGC処理を少しは削減できるはずです。

benchmark:
  baseline: "Object.new"
  object_id: "Object.new.object_id"
compare-ruby: ruby 3.5.0dev (2025-04-10T09:44:40Z master 684cfa42d7) +YJIT +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-04-10T10:13:43Z lazy-id-to-obj d3aa9626cc) +YJIT +PRISM [arm64-darwin24]
warming up..

|           |compare-ruby|built-ruby|
|:----------|-----------:|---------:|
|baseline   |     26.364M|   25.974M|
|           |       1.01x|         -|
|object_id  |     10.293M|   14.202M|
|           |           -|     1.38x|

定番の話ですが、最も効率よくコードを高速化する方法は、そのコードが不要であれば呼び出さないようにすることです。

実際の実装を見てみたい方は、以下のプルリクをどうぞ。

参考: Lazily create objspace->id_to_obj_tbl by byroot · Pull Request #13115 · ruby/ruby

🔗 インラインストレージ

メモリとCPUを節約できたのは結構ですが、競合ポイントを大きく削減できたわけではありません。他にできることはあるでしょうか?

この問題の核心は、object_idがグローバルな中央集中型ハッシュテーブルに保存されていることです。これが解消されなければ、同期処理を削除できません。ロックフリーのハッシュテーブルを実装できれば別ですが、これはかなり難しい作業です(John Hawthornがfstring_tableで使ったハッシュセットによる方法よりもずっと困難です)。

しかしもっと重要なのは、全オブジェクトのIDを中央集中型ハッシュテーブルに保存するという従来の方法は、局所性(locality)の観点からもよろしくないという点です。
さらに、オブジェクトのプロパティにアクセスするためにハッシュ探索が発生するのは、コスト上非常に不利です(本来ならオブジェクト内部に直接保存すべきです)。

考えてみれば、object_idもインスタンス変数も、本質的には大差ありません。

module Kernel
  def object_id
    @__object_id ||= ObjectSpace.generate_next_obj_id
  end
end

ID生成はスレッド安全にしておく必要がありますが、これは増分操作をアトミックにするだけで簡単に実現できます。しかしそれ以外については、「そのオブジェクトは複数のRactorからアクセス可能な特殊なオブジェクトではない」という前提のもとで、object_idを保存するという改変操作を、VM全体をロックせずに行えるようになります。

しかし、こんな単純な話では終わらないのが世の常です。

🔗「最終」シェイプ

Ruby 3.2から、オブジェクトのインスタンス変数の保存方法をシェイプ(shape)1で定義するようになりました。

これについても疑似Rubyコードを使って基本的な仕組みを説明しましょう。

まず、シェイプはツリー構造に似た構造を取ります。あらゆるシェイプには親が1つ存在し(rootを除く)、子は0〜N個存在します。

class Shape
  def initialize(parent, type, edge_name, next_ivar_index)
    @parent = parent
    @type = type
    @edge_name = edge_name
    @next_ivar_index = next_ivar_index
    @edges = {}
  end

  def add_ivar(ivar_name)
    @edges[ivar_name] ||= Shape.new(self, :ivar, ivar_name, next_ivar_index + 1)
  end
end

このシェイプを使うことで、Ruby VMが以下のようなコードを実行しなければならなくなったときに、

class User
  def initialize(name, role)
    @name = name
    @role = role
  end
end

以下のようにオブジェクトシェイプの処理を必要に応じて実行できます。

# オブジェクトをアロケーションする
object = new_object
object.shape = ROOT_SHAPE

# @nameを追加する
next_shape = object.add_ivar(:@name)
object.shape = next_shape
object.ivars[next_shape.next_ivar_index - 1] = name

# @roleを追加する
next_shape = object.add_ivar(:@role)
object.shape = next_shape
object.ivars[next_shape.next_ivar_index - 1] = role

この手法は意表をついているように見えるかもしれませんが、実は非常に効率がよいのです。その理由はさまざまですが、これについては以下の別記事にも書いたので、本記事ではこれ以上触れません。ご興味のある方はどうぞ。

Ruby: メモ化のイディオムが現代のRubyパフォーマンスに与える影響(翻訳)

しかし、シェイプに記録されるのは、インスタンス変数の配置方法だけではありません。シェイプは、そのオブジェクトの大きさについてもトラッキングするので、シェイプに保存可能なインスタンス変数の個数や、オブジェクトがfrozenかどうかについてもトラッキングしています。

これも疑似Rubyコードで表すと以下のようになります。

class Shape
  def add_ivar(ivar_name)
    if @type == :frozen
      raise "Can't modify frozen object"
    end
    @edges[ivar_name] ||= Shape.new(self, :ivar, ivar_name, next_ivar_index + 1)
  end

  def freeze
    @edges[:__frozen] ||= Shape.new(self, :frozen, nil, next_ivar_index)
  end
end

つまり、frozenなシェイプはツリーの末端である最終(final)シェイプなのです。frozenタイプのシェイプには子要素が存在しません。

しかしobject_idの場合は、frozenかどうかにかかわらず、あらゆるオブジェクトでオブジェクトIDを保存できるようにする必要があります。すなわち、シェイプを改修する最初の作業は、これを可能にすることです。これについては、私が比較的シンプルなコミット(ca92bbe)で実現しました。

しかし、ここでも少々ややこしい点がありました。Object#dup呼び出しなどいくつかのケースでは、frozenでないシェイプを見つける必要があります。従来はfrozenのシェイプが子を持つことはありえなかったため、実装は以下のように非常にシンプルでした。

class Object
  def dup
    new_object = self.class.allocate
    if self.shape.type == :frozen
      new_object.shape = self.shape.parent
    else
      new_object.shape = self.shape
    end
    # 省略
  end
end

frozenシェイプが子を持つことを許すと、ツリーを駆け上ってfrozenでない直近のシェイプを探索して、引き継ぐすべての子シェイプに再適用する必要があるため、処理が複雑になります。

この小さなリファクタリングを終えたことで、SHAPE_OBJ_IDという新しいタイプのシェイプの導入に成功しました。SHAPE_OBJ_IDの振る舞いは、インスタンス変数のシェイプと非常に似通っています。

class Shape
  def object_id
    # 最初にOBJ_IDシェイプが先祖に存在するかどうかをチェック
    shape = self
    while shape.parent
      return shape if shape.type == :obj_id
      shape = shape.parent
    end

    # 存在しない場合はシェイプを作成する
    @edges[:__object_id] ||= Shape.new(self, :obj_id, nil, next_ivar_index + 1)
  end
end

これと同様に、あらゆるオブジェクトの内部にobject_id保存用のインラインスペースを予約できるようになり、「いくつかのケースでは」完全にロックフリーのオブジェクトIDにアクセス可能になりました。

🔗 ロックフリーシェイプ

いくつかのケースでは」と書いた理由は、まだまだ制約がたくさん残されているからです。

その1: シェイプはほぼイミュータブル(改変不可)であるため、オブジェクトのシェイプやその先祖にアクセスするときにロックを取得する必要がありません。しかし、シェイプの子を探索・作成する場合は、引き続きVMをロックして同期する必要があります。
そういうわけで、私のパッチが適用されたとしても、オブジェクトIDに初めてアクセスするときは引き続きロックが必要です(ロックフリーになるのは以後のアクセスのみです)。

子のシェイプをロックフリーで探索・作成できるようになれば、object_idに限らず多方面で便利になるはずなので、今後実現できると期待しています。まだ具体的なアイデアはありませんが、何らかのソリューションが見つかることを期待しています。
しかし、たとえロックフリーを完全には実現できないとしても、少なくとも専用のロックを使えば、VM全体を同期する他のコードパスと競合せずに、同じ操作を実行するパスのみを処理できると考えています。

その2: オブジェクトがRactor間で共有される可能性がある場合、オブジェクトIDを保存する前に引き続きロックを取得する必要があります(さもないと、同時書き込みによって競合状態が発生する可能性があります)。オブジェクトシェイプを更新して、オブジェクト内にobject_idを書き込む必要があるため、すべてをアトミックに実行することはできません。

その3: オブジェクトがインスタンス変数を保存する方法は、どのオブジェクトでも同じというわけではありません。

🔗 汎用のインスタンス変数

Rubyistなら「Rubyではあらゆるものがオブジェクトである」ことはご存知かと思いますが、だからと言ってあらゆるオブジェクトが対等であるとは限りません。

インスタンス変数の文脈では、オブジェクトは以下の3つに分類できます。

  • T_OBJECT
  • T_CLASST_MODULE
  • その他すべて

T_OBJECTは、BasicObjectクラスを継承する昔ながらのオブジェクトで、インスタンス変数はそのオブジェクトのスロットに直接保存されます(オブジェクトが十分大きい場合)。
オブジェクトに入り切らなくなった場合は、別のメモリ領域がアロケーションされて、そこにインスタンス変数が移動され、オブジェクトスロットにはその補助メモリへのポインタのみが残されます。

T_CLASST_MODULEはすべて、その名が示す通り、それぞれClassクラスとModuleクラスのインスタンスです。
メソッドテーブルや親クラスへのポインタなど多くの情報をトラッキングするため、どちらのオブジェクトも通常のオブジェクトよりサイズがかなり大きくなっています。

>> ObjectSpace.memsize_of(Object.new)
=> 40
>> ObjectSpace.memsize_of(Class.new)
=> 192

そのため、T_CLASST_MODULEではインスタンス変数がオブジェクト内に直接インラインで保存されることはありません。
インスタンス変数は常に補助メモリに保存され、オブジェクトスロットには補助メモリポインタを保存するための専用のスペースが確保されます

# internal/class.h
struct rb_classext_struct {
    VALUE *iv_ptr; // ivはインスタンス変数
    // ...
}

最後は、それ以外のすべてのオブジェクトです(T_STRINGT_ARRAYT_HASHT_REGEXPなど)。
これらのオブジェクトのスロットには、インライン変数を保存できるような空きスペースは用意されておらず、補助メモリへのポインタを保存する空きスペースすらありません。

だとすると、これらのオブジェクトにインスタンス変数を追加するとどうなるでしょうか?はい、当然ハッシュテーブルに保存されます!

その様子を疑似Rubyコードで見てみましょう。

module GenericIvarObject
  class GenericStorage
    attr_accessor :shape
    attr_reader :ivars

    def initialize
      @ivars = []
    end
  end

  def instance_variable_get(ivar_name)
    store = RubyVM.synchronize do
      GENERIC_STORAGE[self] ||= GenericStorage.new
    end

    if ivar_shape = store.shape.find(ivar_name)
      store.ivars[ivar_shape.next_ivar_index - 1]
    end
  end
end

もう既にうすうすお気づきかと思いますが、これもまた別のグローバルなハッシュテーブルであり、アクセスするときは同期が必要です。
つまり、T_OBJECTT_CLASST_MODULE以外のオブジェクトでは、私のパッチによって、あるグローバル同期ハッシュが別のグローバル同期ハッシュに置き換えられてしまいます。

おそらくそういうわけで、元のobject -> idテーブルは残しておく方が望ましそうではあります。これは引き続き検討が必要な点です。

🔗 まとめ

私のパッチはまだ完成していません。「汎用」オブジェクトをどう扱うのがベストなのかについて引き続き考えておく必要がありますし、おそらく実装にもう少し手を加える必要もあるでしょう。もしかすると、最終的にマージされないまま終わる可能性もあります。

しかし私は、この現状について本記事で共有しておきたいと考えました。問題を説明することで問題に対する私の理解も深まりますし、Ractorにおける現時点の最大のボトルネックはobject_idではないと思っているからです。そして何より、ここで説明していることは、Ractorの並列性を高めるうえでどんな作業が必要となるかを示す良いショーケースとなるからです。

このパッチの現状に興味がおありの方は、以下の差分をどうぞ。

参考: Comparing ruby:master...byroot:object_id-in-shape-snapshot · ruby/ruby

他の内部テーブル(シンボル テーブルやさまざまなメソッドテーブルなど)についても、同様の作業が必要です。

関連記事

PitchforkというWebサーバーを作るまでの長い道のり(翻訳)

RubyのRactorとは一体何なのか(翻訳)

RubyのGVLを消し去りたいあなたへ(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。