スタックとヒープについて
はじめに
最近、実務で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)」について詳しく解説します!