Nimのメモリ管理を理解する⑥ ー Rustと比較して
この記事はNimのメモリ管理を理解するシリーズの6作目になります。Nimではコンパイラがソースコードを解析し、スコープと所有権に基づくメモリ管理を開発者が考える必要がなく自動で行います。今回は同じ処理をRustで実装し、それがNimではどう書けるのか、コンパイラはそれをどう変化させるのかを見ていきます。
〜Nimのメモリ管理を理解するシリーズ〜
- Nimのメモリ管理を理解する① ― Nimの新しいGC、ARCについて
- Nimのメモリ管理を理解する② ― Nimのムーブセマンティクス
- Nimのメモリ管理を理解する③ ― GCなしのNim
- Nimのメモリ管理を理解する④ ― ORC - アルゴリズムによるアドバンテージ
- Nimのメモリ管理を理解する⑤ ― ムーブセマンティクスとデストラクタ
- Nimのメモリ管理を理解する⑥ ー Rustと比較して
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()
とすることで、other
にsome_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()
とし、y
はx
のコピーであると明示します。
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)
x
はy
に代入された後で echo x
と呼び出されているため、 y = x
は =copy(y, x)
に変換されます。
一方でy
はz
に代入した後で呼び出されていないため、これは所有権が移動したということになり、その後で=wasMoved(y)
が挿入され、y
のメモリは開放されます。
まとめ
- Nimではソースコードを解析して、スコープと所有権に基づくメモリ管理を自動で行ってくれます。
- 代入の後で変数呼び出しをしている所はコピーに変換されます。
- 代入の後で変数呼び出しをしていない所は所有権が移動したということになり、その後で
=wasMoved
が挿入され、メモリが開放されます。 - スコープを抜けるとデストラクタが自動的に呼ばれ、GCを使わずメモリが解放されます。
- これにより開発者はプログラムをスクリプト言語のように書け、所有権や変数の寿命を意識する必要はありません。
- しかし内部ではRustと同じメカニズムでメモリ管理を行っています。
Discussion