Posts に戻る

スタックとヒープについて

Rust

はじめに

最近、実務でRustを書く機会が増えてきました。Rustを学ぶ上で避けて通れないのが「所有権」ですが、その背景にあるスタックとヒープという2つのメモリ領域を理解すると、学習効率がぐんと上がります。

今回は、プログラムが実行される際にデータがどこに置かれ、それがパフォーマンスや動作にどう影響するのかを解説します。


1. スタック (Stack)

スタックは、メモリ領域の1つで、「お皿を積み重ね、上から順に取り出す」というLIFO(Last In First Out)の仕組みで動きます。

特徴

  • 非常に高速: 新しいデータを置く場所(プッシュ)も取る場所(ポップ)も常に「一番上」と決まっているため、場所を探す必要がありません。
  • サイズが固定: スタックに保存されるデータは、コンパイル時にサイズが確定している必要があります。

具体的なデータ型

これらはサイズが固定されているため、スタックに直接保存され、コピーも高速です。

  • 整数型 (i32, u32 など)
  • 浮動小数点型 (f64 など)
  • 論理値 (bool)
  • 文字型 (char)

2. ヒープ (Heap)

ヒープは、「レストランのウェイターが客を空いている席に案内する」ような仕組みで動きます。

特徴

  • 柔軟なサイズ: 実行時にどれだけのメモリが必要になるかわからないデータを保存できます。
  • アクセス速度が低速: スタックに比べてアクセスに時間がかかります。

なぜヒープは遅いのか?

ヒープにデータを置く場合、OSは十分な空きスペースを探し、そこを「使用中」としてマークして、その場所を指し示すポインタ(住所)を返します。 プログラムがヒープのデータにアクセスするには、「まずスタックにあるポインタを確認し、その住所を辿ってヒープへ行く」という2ステップが必要になるため、スタックより時間がかかるのです。

具体的なデータ型

  • String型:ユーザー入力などでサイズが動的に変わる文字列。
  • Vec<T>:要素数が実行時に決まる可変長配列。

3. スタック vs ヒープ 比較まとめ

特徴 スタック (Stack) ヒープ (Heap)
データサイズ 固定(コンパイル時に既知) 可変(実行時に決まる)
アクセス速度 非常に高速 低速(ポインタを辿るため)
管理方式 LIFO(自動で整理される) 割り当てと解放が必要
主な型 基本的な型(数値、真偽値) String, Vec<T> など

4. Rustにおける「コピー」と「ムーブ」

Rustでは、データがどちらの領域に置かれるかによって、変数代入時の挙動が変わります。

固定サイズ(スタック)の場合:コピー

スタック上のデータは単純に複製されるため、元の変数も引き続き使えます。

let x = 5;
let y = x; // xの値が y にコピーされる

println!("{}", x); // 5(xはまだ使える)
println!("{}", y); // 5

可変サイズ(ヒープ)の場合:ムーブ

String などのデータは、スタックにある「ポインタ情報」のみが移動します。これをムーブと呼びます。

let s1 = String::from("hello");
let s2 = s1; // s1の所有権がs2に移動(ムーブ)

// println!("{}", s1); // コンパイルエラー:s1は無効になっている
println!("{}", s2); // hello(正常に動作)

なぜムーブするのか? もしコピー(複製)してしまうと、プログラム終了時にヒープ上の同じデータを2回片付けようとしてしまい、メモリの不具合(二重解放)が起きるからです。Rustはこの問題を「所有権」という仕組みで安全に解決しています。


まとめ

  • スタックは、固定サイズのデータを高速に扱う「整理整頓された場所」。
  • ヒープは、動的なデータを柔軟に扱う「広いスペース」。
  • Rustでは、この違いがコピームーブという動作の違いとして現れる。

次回は、このムーブと深く関わるRustの「所有権(Ownership)」について詳しく解説します!

参考文献