0 の object_id は 1、1 は 3、2 は 5 です。
なぜこのようなパターンになるのでしょうか。object_ids のパターンを作成する Fixnums の背後にあるものは何ですか? 0 の ID が 1、1 の ID が 2、2 の ID が 3 であると予想します。
私は何が欠けていますか?
まず最初に、Ruby 言語仕様が s について保証しているのは、空間内で一意であることだけです。それでおしまい。それらは時間的にも一意ではありません。object_id
そのため、特定のobject_id
at the same timeを持つオブジェクトは常に1 つしか存在できませんが、別の時点では、別のオブジェクトにobject_id
s を再利用できます。
完全に正確に言えば、Ruby が保証するのは、
object_id
になりますInteger
object_id
になることはありませんobject_id
その存続期間全体にわたって同じになりますobject_id
あなたが見ているのは、およびFixnum
が YARVでどのように実装されているかの副作用です。これは YARV のプライベートな内部実装の詳細であり、いかなる方法でも保証されていません。他の Ruby 実装では異なる方法で実装されている可能性があり (実際に実装している)、Ruby 実装全体でこれが当てはまるとは限りません。YARV の異なるバージョン間で、または異なるプラットフォーム上の同じバージョンでさえも、真であるとは限りません。
実際、これはごく最近変更されたもので、32 ビット プラットフォームと 64 ビット プラットフォームでは異なります。
YARV では、object_id
単にオブジェクトのメモリ アドレスを返すように実装されています。それはパズルの 1 ピースです。
ナット、なぜ s のメモリアドレスはFixnum
そんなに規則的ですか? 実は、この場合、それらはメモリアドレスではありません! YARV は特別なトリックを使用して、一部のオブジェクトをポインターにエンコードします。実際には使用されていないポインターがいくつかあるため、それらを使用して特定のものをエンコードできます。
これはタグ付きポインター表現と呼ばれ、何十年にもわたって多くの異なるインタープリター、VM、およびランタイム システムで使用されている非常に一般的な最適化トリックです。ほぼすべての Lisp 実装で、それら、多くの Smalltalk VM、多くの Ruby インタープリターなどを使用しています。
通常、これらの言語では、常にオブジェクトへのポインターを渡します。オブジェクト自体は、オブジェクト メタデータ (オブジェクトのタイプ、そのクラス、アクセス制御の制限やセキュリティ アノテーションなど) を含むオブジェクト ヘッダーと、実際のオブジェクト データ自体で構成されます。したがって、単純な整数は、ポインターと、メタデータと実際の整数で構成されるオブジェクトとして表されます。非常にコンパクトな表現でも、単純な整数の場合は 6 バイトのようなものです。
また、そのような整数オブジェクトを CPU に渡して、高速な整数演算を実行することはできません。2 つの整数を追加する場合、実際には、追加する 2つの整数オブジェクトのオブジェクト ヘッダーの先頭を指す 2 つのポインターしかありません。したがって、最初のポインターで整数演算を実行して、整数データが格納されているオブジェクトにオフセットを追加する必要があります。次に、そのアドレスを逆参照する必要があります。2 番目の整数でもう一度同じことを行います。これで、実際に CPU に追加を要求できる 2 つの整数が得られました。もちろん、結果を保持するために新しい整数オブジェクトを作成する必要があります。
したがって、1 つの整数加算を実行するには、実際には3 つの整数加算と 2 つのポインター逆参照と 1 つのオブジェクト構築を実行する必要があります。そして、ほぼ 20 バイトを使用します。
ただし、秘訣は、整数のようないわゆる不変の値型では、通常、オブジェクト ヘッダーにすべてのメタデータを必要としないことです。すべてのものを残して、単純に合成することができます (これは VM-nerd-誰かが見たいと思っているときは、「偽物」を代弁してください)。fixnum には常にclassFixnum
があり、その情報を個別に保存する必要はありません。誰かがリフレクションを使用して fixnum のクラスを見つけた場合、単に返信するだけFixnum
で、その情報を実際にオブジェクト ヘッダーに格納していないこと、実際にはオブジェクト ヘッダー (または物体)。
したがって、トリックは、オブジェクトへのポインター内にオブジェクトの値を格納し、2 つを 1 つに効果的に折りたたむことです。
ポインター内に追加のスペース (いわゆるタグ ビット) を実際に持つ CPU があり、ポインター自体内にポインターに関する追加情報を格納できます。「これは実際にはポインターではなく、整数です」などの追加情報。例としては、Burroughs B5000、さまざまな Lisp マシン、または AS/400 が含まれます。残念ながら、現在の主流の CPU のほとんどにはその機能がありません。
ただし、回避策があります。現在の主流の CPU のほとんどは、アドレスがワード境界に揃えられていない場合、動作が大幅に遅くなります。アライメントされていないアクセスをまったくサポートしていないものもあります。
これが意味することは、実際には、すべてのポインターが 4 (32 ビット システムでは、64 ビット システムでは 8) で割り切れるということです。つまり、常に2 (64 ビット システムでは 3)0
ビットで終了します。 . これにより、実際のポインター ( で終わる00
) と実際には変装した整数であるポインター ( で終わるもの)を区別することができます1
。10
そして、他のことを自由に行うためのすべてのポインターが残っています。また、最新のオペレーティング システムのほとんどは、非常に低いアドレスを自分用に予約しているため、別の領域をいじることができます (たとえば、24 で始まり、で0
終わるポインター00
)。
したがって、31 ビット (または 63 ビット) の整数をポインターにエンコードするには、1 ビット左にシフトして加算1
するだけです。そして、それらを適切にシフトするだけで、非常に高速な整数演算を実行できます (場合によってはそれさえ必要ありません)。
これらの他のアドレス空間をどうするか? まあ、典型的な例にはfloat
、他の大きなアドレス空間での s のエンコードとtrue
、false
、nil
、 、127 個の ASCII 文字、一般的に使用されるいくつかの短い文字列、空のリスト、空のオブジェクト、空の配列などのような多くの特別なオブジェクトが含まれます。0
住所。
YARV では、整数は上で説明した方法でfalse
エンコードされ、アドレスとしてエンコードされ0
(たまたまCでの の表現でもありfalse
ます)、true
アドレスとして(たまたま1 ビットシフトされ2
た C の表現です)、として。true
nil
4
YARVでは、特定の特殊なオブジェクトをエンコードするために次のビット パターンが使用されます。
xxxx xxxx … xxxx xxx1 Fixnum
xxxx xxxx … xxxx xx10 flonum
0000 0000 … 0000 1100 Symbol
0000 0000 … 0000 0000 false
0000 0000 … 0000 1000 nil
0000 0000 … 0001 0100 true
0000 0000 … 0011 0100 undefined
Fixnum
s は 1 つの機械語に収まる 63 ビットの整数であり、flonum
s は 1 つの機械語に収まる 62 ビットFloat
の s です。false
、nil
およびtrue
はundefined
、実装内でのみ使用され、プログラマーには公開されない値です。
32 ビット プラットフォームではflonum
s は使用されない (30 ビットFloat
s を使用しても意味がない) ため、ビット パターンが異なることに注意してください。たとえば、64ビット プラットフォームとnil.object_id
は4
異なり、32 ビット プラットフォーム上にあります。8
それで、あなたはそれを持っています:
object_id
sに使用されますしたがって
object_id
sがありますFixnumi
の場合、object_id はi * 2 + 1
です。
の object_id について0, 2, 4
、それらは何ですか? それらは、、、false
ルビーです。true
nil