【Rust】「Ownership?所有権?何それ・・・」を解決する
所有権(Ownership)とは?
Rustには「所有権(Ownership)」という仕組みがあります。これは、メモリ安全を保証しながらGCなしでリソース管理するためのものです。CやC++のように手動でメモリを解放するのではなく、所有権のルールに従うことで、安全なメモリ管理を実現できます。
この概念により、Rustではコンパイル時にメモリ管理の問題を防ぐことができます。
Rustでは、変数や関数の引数・戻り値がデータの所有者となるのが特徴です。
一方、多くの言語(例えばRuby)では、値は特定の変数に所有されず、自由にコピーできます。
Rubyの例
Rubyでは、同じデータを複数の変数で扱っても、所有者という概念がないため問題なく動作します。
s1 = 'Hello, World!!'
s2 = s1
puts s1
このコードは、s1 の値は s2 に代入されましたが、どちらの変数からも Hello, World!!
にアクセスすることができます。
Rubyでは所有権の概念がないため、値の所有者は s1 でも s2 でもありません。
次のコードはどうでしょうか。
def print_string(s)
puts s
end
s1 = 'Hell, World!!'
print_string(s1)
puts s1
このコードもRubyでは問題なく動作します。
最終的な出力は
Hello, World!!
Hello, World!!
となります。
Rustの例
fn main() {
let s1 = String::from("Hello, World!!");
let s2 = s1;
println!("{}", s1); // コンパイルエラー!
}
このコードがエラーになる理由は、「所有権」が移動するためです。
-
s1 は
Hello, World!!
というデータの所有者。つまり s1 はHello, World!!
の所有権を持っている。 -
let s2 = s1;
によって 所有権が s1 から s2 に移動。Hello, World!!
の所有者は s2 になる。 - s1 はどのデータの所有者でもない、所有権を持たない変数となる。
- そのため、
println!("{}", s1)
の部分でコンパイルエラーになる。
Rubyの例同様に、関数の引数にデータを渡し場合の例を考えます。
fn print_string(s: String) {
println!("{}", s);
} // s はスコープを抜けると解放される
fn main() {
let s1 = String::from("Hello, World!!");
print_string(s1);
println!("{}", s1); // コンパイルエラー!
}
このコードもエラーになります。その理由は「所有権」が関数の引数に移動したためです。
-
s1 は
Hello, World!!
というデータの所有者。つまり s1 はHello, World!!
の所有権を持っている。 -
print_string(s1);
によって 所有権が s1 から print_string関数の引数s に移動。Hello, World!!
の所有者は s になる。 - s1 はどのデータの所有者でもない、所有権を持たない変数となる。
- そのため、
println!("{}", s1)
の部分でコンパイルエラーになる。
clone()による所有権の維持
clone()を使用することで所有権を維持しながら他の変数へデータをコピーすることができます。
fn main() {
let s1 = String::from("Hello, World!!");
let s2 = s1.clone();
println!("{}", s1);
println!("{}", s2);
}
このコードでは、clone() を使用したことで s1 の所有権は維持されたまま、s2 にデータをコピーしています。そのため、s1 も s2 も有効なまま使用可能です。
同様に関数に渡す場合も使用できます。
fn print_string(s: String) {
println!("{}", s);
}
fn main() {
let s1 = String::from("Hello, World!!");
print_string(s1.clone());
println!("{}", s1);
}
これで、Ruby同様の出力が得ることができるようになります。
ただし、clone()関数を無闇に扱うのも危険です。
clone()関数の闇
clone()
はヒープメモリにあるデータを完全にコピーするため、処理コストがかかります。少量のデータであればさほど影響は無いいですが、大量のデータを含むStringなどを clone()
を使用してコピーすると処理コストが発生します。
どのくらい処理に時間がかかるか、またデータ量でどのくらい変わるのかを計測してみます。
use std::time::Instant;
fn main() {
let sizes = [10_000, 100_000, 1_000_000, 10_000_000, 100_000_000];
for &size in &sizes {
let data: Vec<i32> = (0..size).collect();
let start = Instant::now();
let _cloned_data = data.clone();
let duration = start.elapsed();
println!("データサイズ: {:>10}, clone() にかかった時間: {:?}", size, duration);
}
}
計測結果はこのようになりました。
データサイズ | コピーにかかった時間 |
---|---|
1_000 | 167ns |
10_000 | 3.959µs |
100_000 | 30.875µs |
1_000_000 | 500.792µs |
10_000_000 | 3.808625ms |
100_000_000 | 40.6875ms |
結果から分かる通り、データ量が増えれば増えるほど処理時間も増えているのがわかります。
所有権を失わず、処理コストを抑えて変数や関数の引数に値を渡したい場合はどうすればいいのか?
- 「借用(Borrowing)」を使用する。
借用(Borrowing)とは?
Rustでは、「所有権が1つの変数にしか存在できない」ため、関数に値を渡すと所有権が移動します。所有権を移さないために毎回 clone()
をしていたら効率が悪くなります。そこで、Rustは
「所有権を移動せずにデータを一時的に借りる(Borrowing)」という仕組みを提供しています。借用を使えばデータをコピーせずに、また所有権を移動させずに、関数に値を渡すことができます。
借用の種類
- 変更不可(イミュータブル)な借用
- 変更可能(ミュータブル)な借用
イミュータブルな借用の例
「読み取り専用」の借用です。&T
を使用します。
fn print_string(s: &String) {
println!("{}", s);
}
fn main() {
let s1 = String::from("Hello, World!!");
print_string(&s1);
println!("{}", s1);
}
clone()関数を使用したときと同様な結果を得ることができるようになりました。
これは &s1 を渡すことで、s1 の所有権を移さずに関数でデータを参照していることになります。Hello, World!!
の所有権は常に s1 にあるので、 println!("{}", s1);
でも使用可能になっています。
ミュータブルな借用の例
「書き込み」も可能になります。 &mut
を使用します。
fn modify_and_print_string(s: &mut String) {
s.push_str(", World!!");
println!("{}", s);
}
fn main() {
let mut s1 = String::from("Hello");
modify_and_print_string(&mut s1);
println!("{}", s1);
}
&mut s1 を関数に渡すことで、関数の中でデータを書き換えることができるようになりました。ただし、ここで注意が必要なのが、関数内で変更した値が s1 にも反映されます。これは、借用が基本的にはメモリアドレス(ポインタ)渡し、受け取り側はそのメモリアドレスを参照し、データを使用しているためです。
clone() vs 借用
clone()
はヒープメモリにあるデータを完全にコピーするため、処理コストがかかるという説明をしました。処理コストを抑えるために借用を使用するという説明をしました。
果たして、「clone()関数と借用でどのくらいの差になるのか」実験をしてみます。
use std::time::Instant;
fn process_clone(data: Vec<i32>) {
let _processed = data; // cloneしたデータを使用(ダミー)
}
fn process_borrow(data: &Vec<i32>) {
let _processed = data; // 借用したデータを使用(ダミー)
}
fn main() {
let sizes = [10_000, 100_000, 1_000_000, 10_000_000, 100_000_000];
for &size in &sizes {
let data: Vec<i32> = (0..size).collect();
// clone() の計測
let start = Instant::now();
process_clone(data.clone());
let clone_duration = start.elapsed();
// 借用の計測
let start = Instant::now();
process_borrow(&data);
let borrow_duration = start.elapsed();
println!(
"データサイズ: {:>10}, clone() 時間: {:?}, 借用 時間: {:?}",
size, clone_duration, borrow_duration
);
}
}
計測結果はこのようになりました。
データサイズ | コピーしたデータの処理時間 | 借用したデータの処理時間 |
---|---|---|
1_000 | 167ns | 42ns |
10_000 | 3.959µs | 42ns |
100_000 | 30.875µs | 42ns |
1_000_000 | 500.792µs | 42ns |
10_000_000 | 3.808625ms | 42ns |
100_000_000 | 40.6875ms | 42ns |
実験からわかることとして、
-
clone()
を使用した方だとデータ量が増えれば増えるほど処理時間も増加する - 借用はデータ量が増えても処理速度は一定
- 可能な限り 借用を使うことで、メモリ効率を改善可能
→ コピーをする必要がない場合は借用を使用する方が良いことがわかります。
まとめ
- Rustは「所有権」により、安全にメモリ管理を行う
- 所有権を渡したくないときは「借用」を使う
- clone() はデータをコピーするのでコストが高い
- 可能な限り clone() ではなく借用を活用することで、パフォーマンスを向上できる
Discussion