ねののお庭。

かりかりもふもふ。

【C#】TypeHandle について。

TypeHandle について知りたい、そうでしょう?(CV:井上麻里奈)

という事で (?)、TypeHandle について纏めておくと地味に便利そうだったので書いておくこととしました。

オブジェクトのメモリレイアウト

以下のような class があったとします。

class MyClass
{
    public int Value 99;
}

この MyClass を new した時、オブジェクトは heap 上に以下のようなメモリレイアウトで配置されています。

Header の後ろに TypeHandle が続き、その後ろにフィールドが確保されています。 64 bit 環境であれば Header, TypeHandle は共に 8 byte です。

TypeHandle とは何か?

この TypeHandle は一体何かについて結論から言ってしまえば「MethodTable へのポインタ」であり、結果的に「Type (型) を一意に識別するための識別子」としても扱える代物です。 まぁぶっちゃけ何を言っているんだという感じだと思うので、順を追ってみていきましょう。

まず、.NET では Type 毎に MethodTable というものが1つ存在します。 この MethodTable には Type に関する様々な情報が格納されていて、たとえば該当の Type の種別 (abstract class, concrete class, struct, interface 等) や、親クラスに関する情報、実装されている interface の数など、様々な情報が含まれています。 実際にどのような情報が含まれているかはコードから読み取れます

そして TypeHandle にはこの Type に関する情報が含まれている MethodTable へのポインタが格納されています。 なので runtime はオブジェクトの Type に関する情報を高速に引き出せるようになっているわけです。 また Type と MethodTable は一対一に対応するため、MethodTable へのポインタの値は実質的に「Type を一意に識別するための識別子」としても機能するわけです。

ここで具体的に MethodTable が使われているところを軽く覗いてみましょう。 たとえば obj is MyClass みたいな形でキャストが行われた場合、内部的には以下の IsInstanceOfClass が用いられます。このメソッドでは変換先の Type の TypeHandle とオブジェクトの MethodTable のポインタを比較していたり、MethodTable の ParentMethodTable を参照して親クラスの MethodTable へのポインタ取得して云々といった処理がなされている事が分かるかと思います。

// https://github.com/dotnet/runtime/blob/v9.0.10/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/CastHelpers.cs#L132
private static object? IsInstanceOfClass(void* toTypeHnd, object? obj)
{
    if (obj == null || RuntimeHelpers.GetMethodTable(obj) == toTypeHnd)
        return obj;

    MethodTable* mt = RuntimeHelpers.GetMethodTable(obj)->ParentMethodTable;
    for (; ; )
    {
        if (mt == toTypeHnd)
            goto done;

        if (mt == null)
            break;

        mt = mt->ParentMethodTable;
        if (mt == toTypeHnd)
            goto done;

        if (mt == null)
            break;

        mt = mt->ParentMethodTable;
        if (mt == toTypeHnd)
            goto done;

        if (mt == null)
            break;

        mt = mt->ParentMethodTable;
        if (mt == toTypeHnd)
            goto done;

        if (mt == null)
            break;

        mt = mt->ParentMethodTable;
    }

    obj = null;

done:
    return obj;
}

実際には is を使ったキャストなどは Dynamic PGO による最適化が効くので IsInstanceOfClass は呼び出されなかったりします。 このあたりはこの記事からは話が逸れてしまうので、詳しく知りたい方はこちらの資料をご覧ください。

オブジェクトの参照が指し示す先

参照型はオブジェクトへの参照を取りまわすから参照型なわけです。 さて、この参照もといポインタはオブジェクトのどこを指し示しているでしょうか?

素朴に考えれば Header を指しているのではないか?と考えてしまうかもしれませんが、それは間違いです。 実際には TypeHandle へのポインタとなっています。

Microsoft の開発者ブログ から引用

これは無理やり参照型のオブジェクトのポインタをすっぱ抜いて、ポインタが指している値を 8 byte 分取得してみると、TypeHandle の値と等しくなっている事が手元でも確認できるかと思います。 なお TypeHandle の値は合法的に Type.TypeHandle で取得する事が可能です。

ちなみにこの知識は JIT が吐き出す x86-64 asm を読むうえで重要というか、知らないと asm を正しく読めなかったりするので、頭に入れておくと良いでしょう。 まぁ asm 読む人間がどれだけいるんだ、という話ではあるのですが。

より厳密な話

TypeHandle は「MethodTable へのポインタである」と書きましたが、厳密には少し不正確だったりします。 なぜなら TypeHandle の領域は GC にも別用途で活用されているためです。

.NET の GC は Mark & Sweep 方式を取っています。 さて、マークするとは一体どこにされていると思いますか? 話の流れでお分かりになるかと思いますが、そう、実は TypeHandle の領域を用いています。 正確には TypeHandle の下 1 bit にマークしています。 これは面白いテクニックで、.NET の heap は 32/64 bit 環境でそれぞれ 4/8 byte にアライメントされるので、ポインタの下 2/3 bit が必ず 0 になります。 この必ず 0 になるという事を利用して、GC でマークする際には TypeHandle の下 1 bit にマークするという事を行っているわけです。賢い。 なので実際 64 bit 環境においては、MethodTable を取得する際には ~0x7 でマスクして取り出し、GC にマークされているかどうかは 0x1 でマスクして取り出しています

まとめ

  • TypeHandle は MethodTable へのポインタ
    • ただし厳密には TypeHandle の下 2/3 bit は GC に使われていたりする
  • TypeHandle は Type を一意に識別するための識別子として使える
  • オブジェクトの参照が指し示す先は Header ではなく TypeHandle

References