👑

Nimのメモリ管理を理解する⑥ ー Rustと比較して

2024/08/14に公開

この記事はNimのメモリ管理を理解するシリーズの6作目になります。Nimではコンパイラがソースコードを解析し、スコープと所有権に基づくメモリ管理を開発者が考える必要がなく自動で行います。今回は同じ処理をRustで実装し、それがNimではどう書けるのか、コンパイラはそれをどう変化させるのかを見ていきます。


〜Nimのメモリ管理を理解するシリーズ〜


Nimではコンパイルオプションに --expandArc を指定することで、コンパイラがどのようにソースコードを変化させたのかを確認することができます。
この機能を使ってコンパイラが変化させた後のソースコードを確認してみます。

--expandArc:PROCNAME show how PROCNAME looks like after diverse optimizations before the final backend phase (mostly ARC/ORC specific)

コピー

Rustではこのコードはコンパイルエラーになります

fn main() {
    let mut some_numbers = vec![1, 2];
    let other = some_numbers;
    some_numbers.push(3);
    println!("{:?}", other);
    println!("{:?}", some_numbers);
}
error[E0382]: borrow of moved value: `some_numbers`
 --> src/main.rs:5:5
  |
2 |     let mut some_numbers = vec![1, 2];
  |         ---------------- move occurs because `some_numbers` has type `Vec<i32>`, which does not implement the `Copy` trait
3 |     // let other = some_numbers.clone();
4 |     let other = some_numbers;
  |                 ------------ value moved here
5 |     some_numbers.push(3);
  |     ^^^^^^^^^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
4 |     let other = some_numbers.clone();
  |                             ++++++++

some_numbersの所有権は other に移動しているため、some_numbersに対して操作を行おうとしているとエラーになります。
また、some_numbers.clone()とすることで、othersome_numbersの複製を作成するように言われます。
このように、Rustでは所有権の移動を明示的に行う必要があります。

つまりこのように書く必要があります。

fn main() {
    let mut some_numbers = vec![1, 2];
    let other = some_numbers.clone();
    some_numbers.push(3);
    println!("{:?}", other);
    println!("{:?}", some_numbers);
}

Nimではこのように書けます。

proc main() =
  var someNumbers = @[1, 2]
  let other = someNumbers
  someNumbers.add(3)
  echo other
  echo someNumbers

これをNimのコンパイラは以下のように変換します。

proc main() =
  var
    someNumbers
    other
  try:
    someNumbers = @[1, 2]
    `=copy`(other, someNumbers)
    add(someNumbers, 3)
    echo [`$`(other)]
    echo [`$`(someNumbers)]
  finally:
    `=destroy_1`(other)
    `=destroy_1`(someNumbers)

other = someNumbers=copy(other, someNumbers) になり、代入が自動でコピーへと変換されます。
またスコープを抜けた後は =destroy_1(other)のように自動的にデストラクタが呼ばれ、メモリが解放されます。これによりGCを使わず効率的なメモリ管理を行っています。

暗黙的なムーブ

Rustではこのコードはエラーになります。

fn main() {
    let x = vec![1,2,3];
    let y = x;
    println!("{:?}", x);
    let z = y;
    println!("{:?}", z);
}
error[E0382]: borrow of moved value: `x`
 --> src/main.rs:4:22
  |
2 |     let x = vec![1,2,3];
  |         - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait
3 |     let y = x;
  |             - value moved here
4 |     println!("{:?}", x);
  |                      ^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let y = x.clone();
  |              ++++++++

xの所有権はyに移動したため、その後でxを参照しようとしているとエラーになります。
y = x.clone()とし、yxのコピーであると明示します。
yの所有権はzへ移動します。
最終的にはこのようになります。

fn main() {
    let x = vec![1,2,3];
    let y = x.clone();
    println!("{:?}", x);
    let z = y;
    println!("{:?}", z);
}

同じことをNimで書くとこのようになります。

proc main() =
  var x = @[1,2,3]
  var y = x
  echo x
  var z = y
  echo z

これをコンパイラはこのように変換します。

proc main() =
  var
    x
    y
    z
  try:
    x = @[1, 2, 3]
    `=copy`(y, x)
    echo [`$`(x)]
    z = y
    `=wasMoved`(y)
    echo [`$`(z)]
  finally:
    `=destroy_1`(z)
    `=destroy_1`(y)
    `=destroy_1`(x)

xyに代入された後で echo xと呼び出されているため、 y = x=copy(y, x) に変換されます。
一方でyzに代入した後で呼び出されていないため、これは所有権が移動したということになり、その後で=wasMoved(y)が挿入され、yのメモリは開放されます。

まとめ

  • Nimではソースコードを解析して、スコープと所有権に基づくメモリ管理を自動で行ってくれます。
    • 代入の後で変数呼び出しをしている所はコピーに変換されます。
    • 代入の後で変数呼び出しをしていない所は所有権が移動したということになり、その後で=wasMovedが挿入され、メモリが開放されます。
    • スコープを抜けるとデストラクタが自動的に呼ばれ、GCを使わずメモリが解放されます。
  • これにより開発者はプログラムをスクリプト言語のように書け、所有権や変数の寿命を意識する必要はありません。
  • しかし内部ではRustと同じメカニズムでメモリ管理を行っています。
GitHubで編集を提案

Discussion