てくメモ

trivial な notes

【C#】スタック上に class インスタンスを錬成する

以下のポストがきっかけ。

上記ポストはざっくりと、「メモリマップトファイルみたいなものをダイレクトにクラスにしたとして、GCにその領域をマネージドヒープとして登録できるよ」という内容。

ただGCの話よりも個人的に気になったのが、例として示したコードでスタック上にclassインスタンスをでっち上げていること。コンストラクタをバイパスして実体を与える行為は、まさに「錬成」。

ポストで紹介されているAPIは、指定した領域を "Frozen Segment" として登録するもの。スタックのスコープの中で扱う限りは、GC関係の処理はなくても問題ないはず。

というわけで、錬成を抜き出して試してみる。

クラスインスタンスの構造

錬成をするにあたって、まず構造のおさらい。

クラスインスタンスは、次のような構造になっている。

  1. オブジェクトヘッダー(IntPtrのサイズ)
  2. メソッドテーブルのポインタ(IntPtrのサイズ)
  3. フィールドの内容

参考:Managed object internals, Part 1. The layout - Developer Support

つまり最低限、64 bit であれば 8バイト + 8バイト + フィールドのサイズが必要なサイズ。ここにパディングが絡み、メモリレイアウトが決定する。

オブジェクトヘッダーについて。冒頭ポストでも何かを設定することはしていないが、この領域はlockに使われる Sync Block であり、個別で何かを設定する必要はないようだ。(詳細未調査)

錬成

次のようなクラスを考える。

private class ConfigClass
{
    public int Value1;
    public int Value2;

    public override string ToString() => $"({Value1}, {Value2})";
}

試しなのでパディングの入らないクラスとした。サイズは 8 + 8 + (4 + 4)。

ちなみにConfigClassというのは、使い捨てるclass、と考えて浮かんだのがコンフィグ的なものだったため。

それでは錬成。

unsafe
{
    byte* mem = stackalloc byte[
        8 // オブジェクトヘッダー
        + 8 // メソッドテーブル
        + 4 + 4]; // フィールド
    byte* objStart = mem + 8;

    // メソッドテーブルのポインタをセット
    *(IntPtr*)objStart = typeof(ConfigClass).TypeHandle.Value;

    // 正しくオブジェクトを錬成できたか確認するため、あらかじめフィールドに値をセットしておく
    mem[16] = 8;
    mem[20] = 16;

    var instance = *(ConfigClass*)&objStart;

    Console.WriteLine(instance);
    // (8, 16) ... フィールドの値は期待通りで、オーバーライドした ToString もちゃんと呼ばれている

    instance.Value2 = 512;
    Console.WriteLine(instance);
    // (8, 512) ... フィールドの書き込みもできている

    Console.WriteLine(GC.GetGeneration(instance));
    // 2147483647 ... GC管理から外れている
}

クラスインスタンスとして振る舞うものを、スタック上につくり出すことができた。

ベンチマーク

せっかくなので、ベンチマークも行う。(BenchmarkDotNet)

[ShortRunJob]
[MemoryDiagnoser]
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class StackClassBenchmark
{
    private class Class
    {
        public int Value1;
        public int Value2;
    }

    [Benchmark(Baseline = true)]
    public int Heap() => new Class().Value1;

    [Benchmark]
    public unsafe int Stack()
    {
        byte* mem = stackalloc byte[24];
        byte* objStart = mem + 8;
        *(IntPtr*)objStart = typeof(Class).TypeHandle.Value;
        var instance = *(Class*)&objStart;
        return instance.Value1;
    }
}
| Method | Mean     | Ratio | Gen0   | Allocated | Alloc Ratio |
|------- |---------:|------:|-------:|----------:|------------:|
| Heap   | 3.440 ns |  1.00 | 0.0076 |      24 B |        1.00 |
| Stack  | 1.353 ns |  0.39 |      - |         - |        0.00 |

newしていないので当たり前といえば当たり前だが、アロケーションなし。

typeof(T).TypeHandle.Valueが少し気になっていたが、見る限り負荷的な落とし穴もなさそう。

むすび

まず何より危険。また、普通にnewするのに対する記述の冗長さや、レイアウトを踏まえたサイズが要求されるなど、気軽さもない。実践での利用はさすがに、といった感じ。

ただ、クラスを錬成するというのは今までまったく考えたことがなかったので、刺激的だった。