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)。
🔗 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以降は8
や16
といった桁数の小さい整数値が出力されます。
この変更により、従来の_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_id
1つあたり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コードではそれほど使われていません。そのことはほぼ事実なのですが、以下のように現実のコードで使われているのも確かです。
- mail gemの利用例
- rubocopでの利用例
- Railsでの利用例(他にも多数)
しかし、オブジェクト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);
}
String
やArray
などのよく使われる値クラスは、オブジェクト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
この手法は意表をついているように見えるかもしれませんが、実は非常に効率がよいのです。その理由はさまざまですが、これについては以下の別記事にも書いたので、本記事ではこれ以上触れません。ご興味のある方はどうぞ。
しかし、シェイプに記録されるのは、インスタンス変数の配置方法だけではありません。シェイプは、そのオブジェクトの大きさについてもトラッキングするので、シェイプに保存可能なインスタンス変数の個数や、オブジェクトが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_CLASS
、T_MODULE
- その他すべて
T_OBJECT
は、BasicObject
クラスを継承する昔ながらのオブジェクトで、インスタンス変数はそのオブジェクトのスロットに直接保存されます(オブジェクトが十分大きい場合)。
オブジェクトに入り切らなくなった場合は、別のメモリ領域がアロケーションされて、そこにインスタンス変数が移動され、オブジェクトスロットにはその補助メモリへのポインタのみが残されます。
T_CLASS
とT_MODULE
はすべて、その名が示す通り、それぞれClass
クラスとModule
クラスのインスタンスです。
メソッドテーブルや親クラスへのポインタなど多くの情報をトラッキングするため、どちらのオブジェクトも通常のオブジェクトよりサイズがかなり大きくなっています。
>> ObjectSpace.memsize_of(Object.new)
=> 40
>> ObjectSpace.memsize_of(Class.new)
=> 192
そのため、T_CLASS
とT_MODULE
ではインスタンス変数がオブジェクト内に直接インラインで保存されることはありません。
インスタンス変数は常に補助メモリに保存され、オブジェクトスロットには補助メモリポインタを保存するための専用のスペースが確保されます
# internal/class.h
struct rb_classext_struct {
VALUE *iv_ptr; // ivはインスタンス変数
// ...
}
最後は、それ以外のすべてのオブジェクトです(T_STRING
、T_ARRAY
、T_HASH
、T_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_OBJECT
、T_CLASS
、T_MODULE
以外のオブジェクトでは、私のパッチによって、あるグローバル同期ハッシュが別のグローバル同期ハッシュに置き換えられてしまいます。
おそらくそういうわけで、元のobject -> id
テーブルは残しておく方が望ましそうではあります。これは引き続き検討が必要な点です。
🔗 まとめ
私のパッチはまだ完成していません。「汎用」オブジェクトをどう扱うのがベストなのかについて引き続き考えておく必要がありますし、おそらく実装にもう少し手を加える必要もあるでしょう。もしかすると、最終的にマージされないまま終わる可能性もあります。
しかし私は、この現状について本記事で共有しておきたいと考えました。問題を説明することで問題に対する私の理解も深まりますし、Ractorにおける現時点の最大のボトルネックはobject_id
ではないと思っているからです。そして何より、ここで説明していることは、Ractorの並列性を高めるうえでどんな作業が必要となるかを示す良いショーケースとなるからです。
このパッチの現状に興味がおありの方は、以下の差分をどうぞ。
参考: Comparing ruby:master...byroot:object_id-in-shape-snapshot · ruby/ruby
他の内部テーブル(シンボル テーブルやさまざまなメソッドテーブルなど)についても、同様の作業が必要です。
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。