.NET で学ぶ “オブジェクトのメモリ配置” 超入門
目次
― ターン制バトル RPG サンプルを題材に―
TL;DR
- スタックには「参照値」だけ、ヒープに“本体”という二段構造をまず押さえよう。
- 継承を使っても オブジェクトは 1 個。基底→派生のフィールドが “連結” されるだけで、ヘッダーは増えない。
- 「継承はメモリ的に重い」という誤解は、“設計論” と “物理配置” を混同しがちなことが原因。
1. .NET 実行時のメモリ 3 レイヤー
領域 | 役割 | 本記事の例 |
---|---|---|
スタック | スレッドごとの作業台。メソッド呼び出しごとにフレームを積み、ローカルには 参照値 を置く | battle, actor, target などの変数(中身はヒープ上のアドレス) |
マネージドヒープ | new した 参照型本体、配列、string が並ぶ。GC が世代別に回収 | Battle / List<Character> / Player / “勇者アリス" など |
値型の実体 | 「置かれた場所」にインライン配置。フィールドなら親オブジェクトの内部、ローカルならスタック | HP, Atk, Level などの int |
2. RPG サンプルで辿るメモリ確保
var battle = new Battle(
new Player("勇者アリス"), // HP40 Atk10
new Enemy("スライム")); // HP30 Atk 8±2
- string “勇者アリス" → ヒープに確保
- Player 本体(ヘッダー16 B + Name参照8 B + HP4 B + Atk4 B + Level4 B)
- Enemy も同様
- Battle 本体(List<Character> 参照のみ)
- List 本体 + 内部 Character[] 配列(2 要素ぶんの参照を書き込み)
結果: スタックにあるのは battle 参照 1 つだけ。
“ヒープ連鎖” がゲーム全体の状態を保持する。
3. 継承 vs. コンポジション ― 実サイズの比較
モデル | ヒープ上のブロック数 | 追加ポインタ | 64-bit での概算サイズ* |
---|---|---|---|
継承class Player : Character | 1 個 | 0 本 | 32 B(ヘッダー16 + Name参照8 + HP4 + Atk4 + Level4) |
コンポジションclass Player { Character core; int Level; } | 2 個 | 1 本 | 64 B(外側32 B + 内側32 B) |
* ポインタ 8 B、int 4 B、8 B アライン想定。
結論 : 継承の方が ヘッダーも間接参照も少なく、“密” でキャッシュフレンドリー。
コンポジションが優れるのは柔軟性・疎結合・テスト容易性など 設計面 の話であり、
物理メモリ効率では継承が有利 なのが CLR の仕様。
4. オブジェクトヘッダーとフィールド並び
Player object (64-bit)
┌─ SyncBlockIndex (4B)
│ GC ロック等
├─ MethodTablePtr (8B)
│ vtable への参照
├─ Name 参照 (8B)
├─ HP (int) (4B)
├─ Atk (int) (4B)
├─ Level (int) (4B)
└─ padding (4B) ← 8B 境界合わせ
- ヘッダー 16 B は全オブジェクト共通
- フィールドは IL 宣言順 → JIT が8 B境界にパディング
- 派生しても MethodTablePtr は 1 本。仮想呼び出しはポインタ間接 1 回のみ
5. よくある誤解と整理
誤解 | 実際 |
---|---|
「継承すると基底オブジェクトが別に確保される」 | × C++ の sub-object のような“入れ子ブロック”は作られない。CLR では 1 ブロックに直結 |
「継承はメモリが重い」 | × ヘッダーは増えず むしろ参照 1 本ぶんだけ軽い。重いのは“設計依存” の話 |
「値型は常にスタック」 | × フィールドに置けば ヒープ上の親オブジェクト内部 に埋め込まれる |
6. 計測して納得しよう
// .NET 8 なら sizeof が参照型も OK
Console.WriteLine(Marshal.SizeOf(typeof(Player))); // 32
Console.WriteLine(Marshal.SizeOf(typeof(Character))); // 24
CLR バージョンやビルド構成(Debug / Release, x86 / x64)で数値は変わる。
“早すぎる最適化” を避けたいならまず計測 が鉄則。
7. まとめ & 次の一歩
- スタック = 参照値、ヒープ = 本体 を最初に頭に入れる
- 継承しても オブジェクトは増えず、コンポジションよりメモリ局所性が良い
- 「継承は重い」は 設計論的デメリット。メモリ効率ではむしろ有利
- 気になったら Marshal.SizeOf や GC.GetGeneration で可視化
- ここを押さえておくと、GC チューニング や 高負荷ゲームのプロファイルにすぐ活かせる
学習課題
- Enemy に Inventory (構造体 or クラス) を加えて 値型 / 参照型 のレイアウト差 を観察
- struct をむやみに増やすと コピーコストで CPU が飽和 するケースも計測
- –gcserver –gcconcurrent などランタイムスイッチで GC ログ を取得し、世代別サバイバル率をグラフ化してみる
このブログを足がかりに、“オブジェクトとメモリ” の裏側を意識してプログラミングを楽しんでみてください。
訪問数 4 回, 今日の訪問数 4回
ディスカッション
コメント一覧
まだ、コメントがありません