🔥

[Rust]所有権によるメモリ管理とは?ガベージコレクションとの比較

2024/08/07に公開1

メモリの前提知識

スタックとヒープ

メモリの領域にはスタック領域とヒープ領域がある。

  • スタック領域
    • データサイズと取り出すデータの位置が固定(後入れ先出し)のため、ヒープ領域よりも高速にアクセスできる
    • 関数呼び出しやローカル変数の割り当てに使用される
    • intなどのプリミティブ型は大きさが決まっているのでスタックにメモリ確保される
  • ヒープ領域
    • データの取り出し順序やデータのサイズなどが固定でないため、スタックのデータへのアクセスよりも低速
    • プログラム実行中に必要なサイズのメモリを動的に確保・解放するのに利用される

New領域とOld領域

メモリのヒープ領域にはNew領域とOld領域がある。
生成後すぐに破棄されるデータはNew領域に、長い寿命をもつデータはOld領域に格納される。

所有権システムについて

Rustのメモリ管理に使われる機能。
Rustの各値は、所有者と呼ばれる変数と対応している。
所有権システムにより、メモリの確保、解放が自動的に管理される。
メモリが確保されるのはコンパイル時

所有権が破棄されるタイミング(メモリが解放されるタイミング)

  • 閉じ波括弧のタイミング
    • {
        let hoge = "hello";
      } // ここの閉じ括弧で変数hogeは有効でなくなる      
      
    • 閉じ波括弧で自動的にdrop関数が呼ばれるようになっている
  • ムーブしたタイミング
    • {
        let hoge1 = String::from("hello");
        let hoge2 = hoge1; // ここでhoge1は有効でなくなっている
      } 
      
    • let hoge2 = hoge1;の代入は、値のコピーではなく所有権の移行がされている
    • コピーしたい場合は明示的にクローンする
      • {
          let hoge1 = String::from("hello");
          let hoge2 = hoge1.clone(); // hoge1もhoge2も有効
        } 
        
    • ただしプリミティブ型はデータの大きさが明確なためクローンしなくてもコピーされる(代入時にムーブしない)

GC(ガベージコレクション)について

自動的に不要となったメモリの領域を解放する機能。
Java、PHPなどさまざまな言語に備わる機能。

GCの種類

  • Scavenge GC
    • New領域のメモリの解放
    • 時間がかからない
  • Full GC
    • Old領域のメモリの解放
    • 時間がかかる

メモリが解放されるタイミング

  • メモリ容量が足りなくなったとき
  • 使用メモリが閾値を超えたとき

所有権システムとGCの違い

  • 所有権システムによるメモリ管理は実装時にメモリの管理が可能(いつメモリ確保していつ解放しているかを明確に把握することができる)
    • その分常に所有権がどこにあるのかを考えて実装する必要があるので実装時の負担は大きめ
  • GCはすべて自動でメモリ管理してくれるので、実装者がメモリを意識する必要がない
    • GCの処理の重さによって実行時の速度に影響が出る
    • いつメモリ解放されるのかを明確に把握することができない

感想

深いねー

参考

Discussion

白山風露白山風露

メモリが確保されるのはコンパイル時

うーん、メモリは当然実行時にならないと割り当てられないので、メモリの確保をコンパイル時に行うという捉え方は普通はしないと思います。確保するタイミングやサイズなどはコンパイル時に決まりますが、Javaなどでもオブジェクトのnewを行うタイミングがコンパイル時に決まるのは同じですし……

所有権が破棄されるタイミング

Rustには Non-lexical lifetimes (NLL) という機能があり、必ずしも閉じ括弧つまりブロック末尾で所有権が破棄されるわけではありません。
例として、

let mut hello = "hello".to_string();
{
    let hoge = hello.as_str();
    // A
    hello = "hello2".to_string(); // B
}

というコードを考えてみます。もし hoge がブロック末尾で所有権を破棄するとしたら、 hello はその間借用されたままなので、B の位置で再代入を行おうとするとエラーになるはずですが、実際にはこのコードはコンパイルを通ります。この場合、 hoge がそれ以降利用されておらず、どこで破棄しても問題ない状態になっているため、 A の位置で破棄されるという挙動になります。
ただし Drop トレイトが実装されている型の変数はmoveされない場合必ずブロック末尾で Drop::drop が呼び出されるため、NLLの影響を受けることはありません。

  • ただしプリミティブ型はデータの大きさが明確なためクローンしなくてもコピーされる(代入時にムーブしない)

プリミティブ型かどうかではなく、 Copy トレイトが実装されているかどうかですね。