blog.dopana

Back

Rust、C++、Goなどあらゆるシステムプログラミング言語において、コンピュータが スタックメモリ(Stack) と ヒープメモリ(Heap) をどのように管理しているかを理解することは、高速で安全なソフトウェアを構築するための絶対的な基礎となります。

graph TD
    A["プロセスのメモリ構造"] --> B["スタックメモリ (Stack)<br/>ローカルフレーム、固定長、LIFO"]
    A --> C["ヒープメモリ (Heap)<br/>動的確保、サイズ可変"]
    B --> B1["超高速(スタックポインタ移動のみ)"]
    B --> B2["コンパイル時にサイズ確定"]
    C --> C1["アロケータによる管理 (malloc/free)"]
    C --> C2["ポインタ経由でデータを参照"]

10歳でもわかる説明(ELI5):オフィスの机 vs 倉庫#

  • スタックメモリ(オフィスの自分の机):
    • いま目の前で作業している書類を置く場所です。
    • 書類は上へ順番に積み重ねられます(LIFO: 後入れ先出し)。
    • 机の広さ(メモリ容量)は1MB〜8MB程度と限られていますが、手を伸ばせば一瞬で書類を手に取れます。
  • ヒープメモリ(会社の大型倉庫):
    • 大きな荷物や、サイズがどんどん増える荷物を保管する場所です。
    • 保管場所が必要になったら、倉庫の管理人(メモリ・アロケータ)に依頼します。
    • 管理人は空いている棚を見つけて記録し、荷物の保管場所が書かれた 引換券(ポインタ・アドレス) を渡してくれます。
    • あなたは引換券だけを机(スタック)に置いておき、荷物が必要になったらその住所を確認して取りに行きます。

1. メモリ構造の視覚化#

スタックとヒープにまたがる典型的なプログラムの例を見てみましょう:

src/main.rs
fn main() {
    // 1. スタック上に完全に配置される値
    let age: i32 = 30; // 4バイト
    let coordinates: (f64, f64) = (10.5, 20.8); // 16バイト

    // 2. スタック上のポインタがヒープ上のバッファを指す
    let mut names = Vec::new();
    names.push(String::from("Alice"));
    names.push(String::from("Bob"));
}
rust

実際のメモリ配置図:

graph LR
    subgraph StackFrame ["STACK (mainスタックフレーム)"]
        A["age = 30 (4B)"]
        B["coords = (10.5, 20.8)"]
        subgraph VecMeta ["names (Vectorメタデータ - 24B)"]
            V_ptr["ptr = 0x7FFF00"]
            V_cap["cap = 2"]
            V_len["len = 2"]
        end
    end
    
    subgraph HeapMemory ["HEAP (動的メモリ領域)"]
        H1["0x7FFF00: String 1 メタデータ"]
        H2["0x7FFF18: String 2 メタデータ"]
        Data1["0x8A0000: 'A', 'l', 'i', 'c', 'e'"]
        Data2["0x8A0020: 'B', 'o', 'b'"]
    end
    
    V_ptr -->|配列を指す| H1
    H1 -->|文字列実体を指す| Data1
    H2 -->|文字列実体を指す| Data2

2. メモリ割り当てのライフサイクル#

sequenceDiagram
    autonumber
    actor Program as プログラム
    participant Stack as スタックポインタ RSP
    participant Allocator as メモリアロケータ
    participant OS as OSカーネル
    
    Note over Program,Stack: スタックへの基本型配置
    Program->>Stack: RSPレジスタを移動(1CPUサイクル)
    Stack-->>Program: 瞬時に確保完了!
    
    Note over Program,Allocator: ヒープへの動的データ確保
    Program->>Allocator: 1024バイトの確保を要求 (malloc/alloc)
    alt フリーリストに空きブロックあり
        Allocator-->>Program: メモリアドレス 0x5A00 を返却
    else 仮想メモリページの追加が必要
        Allocator->>OS: ヒープ境界を拡張 (mmap / brk)
        OS-->>Allocator: 新しいメモリページを割り当て
        Allocator-->>Program: メモリアドレスを返却
    end

3. スタックとヒープの徹底比較表#

比較項目スタックメモリ (Stack)ヒープメモリ (Heap)
サイズ要件コンパイル時にサイズ確定が必須実行時に動的変更・拡大縮小が可能
割り当て速度超高速(ポインタレジスタの増減のみ)探索・管理コストにより比較的低速
解放タイミングスコープ終了時に自動破棄 (Stack Pop)所有権 (RAII)、GC、または手動解放
CPUキャッシュ局所性極めて高い(連続したアドレス領域)ポインタジャンプによるCache Missが発生しやすい
最大容量小さい(1MB〜8MB、StackOverflowのリスク)大容量(物理RAM+スワップ領域の許す限り)
代表的な型i32, bool, 固定長構造体, [T; N]Vec<T>, String, Box<T>, HashMap

4. ヒープ割り当てを削減する最適化戦略#

過度なヒープ確保は、ゲームのフレーム落ちやマイクロサービスのP99遅延の原因になります:

graph TD
    A["ヒープ最適化戦略"] --> B["1. キャパシティの事前予約<br/>Vec::with_capacity(n)"]
    A --> C["2. スタックインライン配列<br/>SmallVec / ArrayVec"]
    A --> D["3. 必要時のみクローン<br/>std::borrow::Cow"]
    A --> E["4. バッファの再利用<br/>Arena Allocator / Object Pool"]
  1. with_capacity による事前確保:要素追加ごとの再確保(realloc)を防止します。
  2. SmallVec / ArrayVec の活用:要素数が少ない間はスタックに保持し、溢れた場合のみヒープを使います。
  3. Cow (Clone-On-Write):変更が必要になるまでイミュータブルな参照を使い回し、不要なメモリ確保を防ぎます。

まとめ#

  • スタックは超高速でLIFO順に整理され、サイズ固定のローカルデータに最適です。
  • ヒープは動的で柔軟なデータ構造を扱えますが、アロケータの管理コストが伴います。
  • 両者の特徴を理解して適切に使い分けることが、ハイパフォーマンスなシステム構築の鍵となります。

参考文献#