rust
string

[Rust] 引数で文字列の参照を受取る場合に&strと&Stringどちらが良いのか

前回 の投稿でコメント頂いた所で、新たな疑問が浮かんだ。

fn puts(s: &str) {
    println!("puts:       {:p}", s);
}

let a = "hoge".to_string();
println!("&String:    {:p}", &a);
println!("UTF-8 addr: {:p}", a.as_ptr()); // StringのUTF-8バイト列へのポインタ
println!("&str:       {:p}", a.as_str()); // Stringから&strを得る
puts(&a);                                 // &Stringから&strへ型強制
println!("&str:       {:p}", &a as &str); // &Stringから&strへ型変換

前回の投稿で

  1. 参照だけでいい場合は、&strを使う
  2. 文字列の中身を書き換えたい場合はStringを使うとよい

という理解に至ったけど、
じゃあ上記 1 の場合 に &str&String で受け取るのは、何が違うのか。

検証環境

OS: macOS High Sierra 10.13.3
Rust: rustc 1.26.0-nightly (c08480fce 2018-03-23)

引数で&strと&Stringを受け取った時のポインタを見てみる

fn puts_str(s: &str) {
    println!("{:p}", s);
}

fn puts_string(s: &String) {
    println!("{:p}", s.as_ptr());
}

let a = "hoge".to_string();
puts_str(&a);
puts_string(&a);

※前回コメント頂いたコードで、as_ptr で見ればいい事を知った!

0x100a17018
0x100a17018

同じですね。

&strで受け取る場合と&Stringで受け取る場合でベンチマーク取ってみる

呼び出し元の変数の型がStringの場合

extern crate test;

fn print_str(s: &str) {
    println!("{:p}", s);
}

fn print_string(s: &String) {
    println!("{:p}", s);
}

#[cfg(test)]
mod tests {
    use super::*;
    use self::test::Bencher;

    #[bench]
    fn bench_string_to_str(b: &mut Bencher) {
        let s = "hoge".to_string();
        b.iter(|| print_str(&s) );
    }

    #[bench]
    fn bench_string_to_string(b: &mut Bencher) {
        let s = "hoge".to_string();
        b.iter(|| print_string(&s) );
    }
}

何度か実行してみた

cargo bench
running 2 tests
test str_vs_string::tests::bench_string_to_str    ... bench:         176 ns/iter (+/- 42)
test str_vs_string::tests::bench_string_to_string ... bench:         176 ns/iter (+/- 37)
running 2 tests
test str_vs_string::tests::bench_string_to_str    ... bench:         174 ns/iter (+/- 65)
test str_vs_string::tests::bench_string_to_string ... bench:         184 ns/iter (+/- 44)
running 2 tests
test str_vs_string::tests::bench_string_to_str    ... bench:         183 ns/iter (+/- 24)
test str_vs_string::tests::bench_string_to_string ... bench:         184 ns/iter (+/- 43)

型強制にはいくつかの規則があるのですが、その中の Deref による型強制 が働いて、&String から &str へ暗黙的に変換されます。

ということだったので、型変換がある分、&strの方が遅いかと思ったけど、
これだけだと、ほぼ変わらなかった。(といっても過言ではない気がする)

呼び出し元の変数の型が&strの場合

extern crate test;

fn print_str(s: &str) {
    println!("{:p}", s);
}

fn print_string(s: &String) {
    println!("{:p}", s);
}

#[cfg(test)]
mod tests {
    use super::*;
    use self::test::Bencher;

    #[bench]
    fn bench_string_to_str(b: &mut Bencher) {
        let s = "hoge".to_string();
        b.iter(|| print_str(&s) );
    }

    #[bench]
    fn bench_string_to_string(b: &mut Bencher) {
        let s = "hoge".to_string();
        b.iter(|| print_string(&s) );
    }

    // 追加
    #[bench]
    fn bench_str_to_str(b: &mut Bencher) {
        let s = "hoge";
        b.iter(|| print_str(&s) );
    }
    // 追加
    #[bench]
    fn bench_str_to_string(b: &mut Bencher) {
        let s = "hoge";
        b.iter(|| print_string(&s.to_string()) );
    }
}
running 4 tests
test str_vs_string::tests::bench_str_to_str       ... bench:         174 ns/iter (+/- 28)
test str_vs_string::tests::bench_str_to_string    ... bench:         217 ns/iter (+/- 49)
test str_vs_string::tests::bench_string_to_str    ... bench:         176 ns/iter (+/- 10)
test str_vs_string::tests::bench_string_to_string ... bench:         177 ns/iter (+/- 29)
running 4 tests
test str_vs_string::tests::bench_str_to_str       ... bench:         176 ns/iter (+/- 41)
test str_vs_string::tests::bench_str_to_string    ... bench:         227 ns/iter (+/- 60)
test str_vs_string::tests::bench_string_to_str    ... bench:         175 ns/iter (+/- 55)
test str_vs_string::tests::bench_string_to_string ... bench:         186 ns/iter (+/- 50)

running 4 tests
test str_vs_string::tests::bench_str_to_str       ... bench:         175 ns/iter (+/- 42)
test str_vs_string::tests::bench_str_to_string    ... bench:         226 ns/iter (+/- 142)
test str_vs_string::tests::bench_string_to_str    ... bench:         185 ns/iter (+/- 53)
test str_vs_string::tests::bench_string_to_string ... bench:         204 ns/iter (+/- 60)

str_to_stringiter の外でto_stringしてしまうと string_to_string と同じことになってしまうので、 iter 内で to_string してみました。
当然、これが一番遅いですね。

ここにもかいてあるけど

しかしながら、パフォーマンス上ではスライスの作成はコピーが発生しませんが、Stringの作成はスライスの内容を一旦ベクタに変換する必要があります

to_string は slice -> Vector なので、メモリ再割当てが発生しているということですかね。

まとめ

  • 呼び出し元の変数の型がStringの場合は、&strでも&Stringでも、さほど変わらないので、受け取る側で使いやすい方で良い
  • 呼び出し元の変数の型が&strの場合は、&Stringで受け取ろうとすると若干不利になる
  • つまり、受け取る側で&strで良いのであれば (Stringでないと困る理由がなければ) &strにしておいた方が効率的

ということだろうか。

結局前回の

参照だけでいい場合は、&strを使う
文字列の中身を書き換えたい場合はStringを使うとよい

と同じことですね。
(というのを再認識した)
(文字列の中身を書き換えたい場合 以外にもStringにしかないメソッドを使いたいとかもあるかも)