以下のポストがきっかけ。
In .NET you can load a huge graph(s) of objects directly from a file and, basically, mmap it & register as a managed heap! GC won't waste time scanning/compacting/collecting it. Quick example of me registering stack memory as a managed heap: pic.twitter.com/ljAn6UNoqS
— Egor Bogatov (@EgorBo) 2023年9月25日
上記ポストはざっくりと、「メモリマップトファイルみたいなものをダイレクトにクラスにしたとして、GCにその領域をマネージドヒープとして登録できるよ」という内容。
ただGCの話よりも個人的に気になったのが、例として示したコードでスタック上にclass
インスタンスをでっち上げていること。コンストラクタをバイパスして実体を与える行為は、まさに「錬成」。
ポストで紹介されているAPIは、指定した領域を "Frozen Segment" として登録するもの。スタックのスコープの中で扱う限りは、GC関係の処理はなくても問題ないはず。
というわけで、錬成を抜き出して試してみる。
クラスインスタンスの構造
錬成をするにあたって、まず構造のおさらい。
クラスインスタンスは、次のような構造になっている。
- オブジェクトヘッダー(
IntPtr
のサイズ) - メソッドテーブルのポインタ(
IntPtr
のサイズ) - フィールドの内容
参考: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
するのに対する記述の冗長さや、レイアウトを踏まえたサイズが要求されるなど、気軽さもない。実践での利用はさすがに、といった感じ。
ただ、クラスを錬成するというのは今までまったく考えたことがなかったので、刺激的だった。