Rustを勉強するときにメモリ周りの知識が必要になることを痛感した。
PythonやGoを使っていると、メモリ周りの知識はあまり必要なかった(筆者が気にしていなかった)ためここにまとめていく。
メモリ領域
プログラムが実行される際には、メモリ領域が必要になる。
メモリ領域は大きく分けて4つあり、それぞれ異なる役割を持つ。
その4つの領域は以下の通り。
コード領域
プログラムの実行コードが格納される領域。
静的領域
グローバル変数や静的変数が格納される領域。
constで定義された定数もここに格納される。
スタック領域
関数の呼び出し時に使用される領域。
関数の呼び出し時に引数やローカル変数が格納される。
ヒープ領域
プログラム実行中に動的に確保される領域。
mallocやnewなどの関数ではここに確保される。
これらメモリ領域のうちコード領域と静的領域はコンパイル時に確定するため、実行時に変更されることはない。
一方で動的に確保されるスタック領域とヒープ領域は実行時に変更されるため、メモリリークやスタックオーバーフローなどの問題が発生する可能性がある。
以上からプログラマが意識すべきはスタック領域とヒープ領域で、本記事ではここにフォーカスする。
メモリの構成
スタック
スタックはLIFO(Last In First Out)のデータ構造で、関数の呼び出し時に使用される。
つまり、関数の呼び出し時に引数やローカル変数がスタックに積まれ(push)、関数の終了時にスタックから取り除かれる(pop)。
そのためスタックはデータを取り扱う際に探査が必要ないため、高速にデータのpush/popができる。
逆にいうと、スタックはデータのサイズが固定であるため、動的なメモリ確保ができずサイズが既知で固定されている必要がある。
ヒープ
ヒープはスタックとは異なり、データのサイズが可変であるため、動的にメモリ確保ができる。
実行時にサイズが不明な場合や、サイズが可変である場合にヒープを使用する。
ヒープはスタックとは異なり、データの探査が必要なためスタックよりも遅い。
Rustにおけるメモリ管理
これまでのプログラミング言語では、メモリ管理の方法は大きく分けて2つある。
- 手動でメモリ管理
CやC++などの言語では、メモリの確保や解放をプログラマが明示的に行う必要がある。
malloc, newなどの関数でメモリを確保し、free, deleteなどの関数でメモリを解放する。
メモリの管理をプログラマが行うため、チューニングがしやすい反面、メモリリークやダブルフリーなどの問題が発生する可能性がある。
- 自動でメモリ管理
JavaやPythonなどの言語では、メモリ管理を自動で行うため、プログラマがメモリ管理を意識する必要がない。
ガーベッジコレクションなどの仕組みにより、メモリリークやダブルフリーなどの問題を回避している一方で、メモリの解放タイミングが不明確になるため、メモリ使用量が増える可能性がある。
Rustは、メモリ管理を自動で行うが、所有権システムを導入することでメモリリークやダブルフリーなどの問題を構造的に回避している。
所有権システム
Rustの変数は所有権を持っており、所有者は必ず1つであり、変数がスコープを抜けるときにメモリが解放される。
スコープとは{}で囲まれたブロックのことで、下の例では変数sはスコープ内で有効であり、スコープを抜けると変数sは無効になる。
{
{
let s = "hello"; // スタック上に確保される文字列リテラル
// sはこのスコープで有効
}
// sはこのスコープで無効でメモリが解放される
}
上記は文字列リテラルの例だが、ヒープ上に確保されたメモリも同様にスコープを抜けるときにメモリが解放される。
{
{
let mut s = String::from("hello"); // ヒープ上に確保される文字列
s.push_str(", world!"); // ヒープ上に確保される文字列を追加
// sはこのスコープで有効
}
// sはこのスコープで無効でメモリが解放される
}
ちなみに文字列リテラルはコンパイル時にサイズが既知で固定されているため、スタック上に確保され、String型は可変なサイズの文字列を扱うため、ヒープ上に確保される。
さらに、所有権システムにより、変数の所有権を別の変数に移すことができる。
以下の例ではs1の所有権がs2に移動して、移動後にs1を参照しようとするとエラーになる。
{
let s1 = String::from("hello");
let s2 = s1; // s1の所有権がs2に移動する
// s1はこのスコープで無効のため下はエラーになる
// println!("{}", s1);
// s2はこのスコープで有効
println!("{}", s2);
}
しかしスタック上に確保される変数はCopyトレイトを実装しているため、所有権を移動しても元の変数を参照できる。
{
let x = 5;
let y = x; // xの所有権がyに移動する
// xはこのスコープで有効
println!("{}", x);
// yはこのスコープで有効
println!("{}", y);
}
参照と借用
所有権システムにより、変数の所有権を移動することでメモリリークやダブルフリーなどの問題を回避できるが、所有権を移動すると元の変数を参照できなくなる。
一方で他のプログラミング言語同様に、所有権を移動せずに変数を参照したい場合がある。
その時には所有権を移動せずに参照を渡すことができる。
Rustでは参照を借用と呼び、借用は不変参照と可変参照の2種類がある。
不変参照は&を使い、可変参照は&mutを使う。
以下の例では、s1の所有権を移動せずにs1の参照をs2に渡している。
{
let s1 = String::from("hello");
let s2 = &s1; // s1の参照をs2に渡す
// s1はこのスコープで有効
println!("{}", s1);
// s2はこのスコープで有効
println!("{}", s2);
}
また可変参照を使うことで、参照先の値を変更することができる。
以下の例では、s1の可変参照をs2に渡して、s2を変更している。
{
let mut s1 = String::from("hello");
// s1はこのスコープで有効
println!("{}", s1);
let s2 = &mut s1; // s1の可変参照をs2に渡す
// s2はこのスコープで有効
s2.push_str(", world!");
println!("{}", s2);
}
注意点として、可変参照は不変参照と同じスコープ内で複数の参照を持つことができない。
つまり以下のようなコードはエラーになる。
{
let mut s1 = String::from("hello");
let s2 = &mut s1; // s1の可変参照をs2に渡す
let s3 = &mut s1; // s1の可変参照をs3に渡す
// s1はこのスコープで有効
println!("{}", s1);
// s2はこのスコープで有効
s2.push_str(", world!");
println!("{}", s2);
// s3はこのスコープで有効
s3.push_str(", world!");
println!("{}", s3);
}
まとめ
Rustは所有権システムを導入することで、メモリリークやダブルフリーなどの問題を構造的に回避している。
所有権への参照と借用を使うことで、所有権を移動せずに変数を参照することができる。