質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.63%

benchテストでの変数の寿命+ArrayをStackとHeapに置いた時の差

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 959

namuyan

score 72

Rustのチュートリアルが完了してからツールを自作できるまで慣れました。
Pyo3の関係でcargo 1.34.0-nightlyを使用しています。

今はNightlyに付属しているベンチ機能を用いてオブジェクトのコスト把握を行っています。
以下のようなコードを用いてスタック領域のArrayとヒープ領域のArrayの速度差を調べようとしました。
しかしコンパイルには失敗します。

#![feature(test)]
extern crate test;

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

    #[bench]
    fn box_ref(b: &mut Bencher){
        let mut data = Box::new([0u8;32768*16]);
        b.iter( move || {data.iter_mut().map(|x| *x += 1)});
    }

    #[bench]
    fn array_ref(b: &mut Bencher){
        let mut data = [0u8;32768*16];
        b.iter(move || data.iter_mut().map(|x| *x += 1));
    }
}


そこで以下の様に変更し計測を行いました。
結果として、Boxが33,809 ns/iter (+/- 10,772)、Arrayが11,342 ns/iter (+/- 509)です。奇妙なのは data.iter_mut...の行の数に依らず計測結果が変わりませんでした。アセンブラを確認しましたが最適化により削除されたのかよくわかりません。

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

    #[bench]
    fn box_ref(b: &mut Bencher){
        fn work() -> Box<[u8;32768*16]> {
            let mut data = Box::new([0u8;32768*16]);
            data.iter_mut().map(|x| *x += 1);
            data
        }
        b.iter(|| work());
    }

    #[bench]
    fn array_ref(b: &mut Bencher){
        fn work() -> [u8;32768*16] {
            let mut data = [0u8;32768*16];
            data.iter_mut().map(|x| *x += 1);
            data
        }
        b.iter(|| work());
    }
}
error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
  --> src\lib.rs:46:30
   |
46 |         b.iter(move || {data.iter_mut().map(|x| *x += 1)});
   |                              ^^^^^^^^
   |
note: first, the lifetime cannot outlive the lifetime  as defined on the body at 46:16...


iterの中にiterを含む事で予測できなくなるのか、原因がよくわからず困っています。
もしこの問題がわかる方がおられましたら、または二つの速度差についてわかる方がおられましたら、
回答の方を宜しくお願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+2

奇妙なのは data.iter_mut...の行の数に依らず計測結果が変わりませんでした。

Rustに慣れてきても間違えることがあるのですが、イテレータのmapメソッドは、forループと違いその場で処理は行ってくれません。実際には新しいイテレータを返します。イテレータを作るだけなら一瞬で終わるので、計測結果に変化が現れなかったわけです。

処理を実行させるにはmapが返したイテレータに対して、collect()sum()などのイテレータを消費するメソッドを適用する必要があります。また、mapの代わりにfor_eachを使うこともできます。

error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
  iterの中にiterを含む事で予測できなくなるのか、原因がよくわからず困っています。

mapが返したイテレータをクロージャの戻り値として返そうとしているためエラーになっています。iter_mut()dataの可変の参照をとります(selfの参照を自動的にとるのでautorefと呼んでます。型強制type coercionという振る舞いの一種です)。しかし、その参照をクロージャの外に渡そうとしているためにライフタイムの要件が競合しているのです(due to conflicting requirements)。

以下のようにmapfor_eachに変えるとうまくいきます。

#![feature(test)]
extern crate test;

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

    #[bench]
    fn box_ref(b: &mut Bencher) {
        let mut data = Box::new([0u8;32768*16]);
        b.iter(move || { 
            data.iter_mut().for_each(|x| *x += 1);
            // data.iter_mut().for_each(|x| *x += 1);
        });
   }

    #[bench]
    fn array_ref(b: &mut Bencher){
        let mut data = [0u8;32768*16];
        b.iter(move || {
            data.iter_mut().for_each(|x| *x += 1);
            // data.iter_mut().for_each(|x| *x += 1);
        });
    }
}

実行結果

$ cargo bench
   Compiling hello-bench v0.1.0 (... /hello-bench)
    Finished release [optimized] target(s) in 0.75s
     Running target/release/deps/hello_bench-27b84eca8ab51126

running 2 tests
test tests::array_ref ... bench:      22,288 ns/iter (+/- 5,191)
test tests::box_ref   ... bench:      23,993 ns/iter (+/- 6,710)

test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out

# コードのコメントを外してfor_eachを2回ずつ実行
$ cargo bench
   Compiling hello-bench v0.1.0 (... /hello-bench)
    Finished release [optimized] target(s) in 0.68s
     Running target/release/deps/hello_bench-27b84eca8ab51126

running 2 tests
test tests::array_ref ... bench:      45,611 ns/iter (+/- 10,786)
test tests::box_ref   ... bench:      47,061 ns/iter (+/- 7,599)

test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out

なお、実際のアプリケーションでは、その前後の処理もあわせて最適化がかかりますので、マイクロベンチマークで測った単体の機能の性能は、ほとんど参考になりません。また、Bencher自体もLLVMの過度な最適化を抑止するような技法を使っているはずですので、その結果をどこまで信用していいのかは私もよくわかりません。

基本的にはプロファイリングなどの手法を使って、アプリケーションのレベルで性能測定することをおすすめします。

たとえば、以下の記事では、macOSのdtraceで収集した情報をflame graphにする方法を紹介しています。

Linuxだとdtraceの代わりにperfが使えたと思います。(私はメインのマシンがFreeBSDとmacOSなのでdtraceしか使っておらず、perfのことはよくわかりません。またWindowsでどうするかもわかりません)


追記

書き忘れましたが、Box::new([0u8;32768*16])のように固定長(サイズが固定)の配列をヒープに置くようなことは普通はしません。スタックに置くのと違って、ヒープに置くときは実行時にサイズを決められますので、このような用途には可変長のベクタ(Vec<T>)を使います。

Box::new([0u8;32768*16])とすると、まずスタック上の配列が初期化され、それがBox::newによってヒープにコピーされますので非効率な処理になります(上のベンチマークではその部分にかかる時間は測定の範囲外になっています) Vec<T>なら最初からヒープ領域が初期化されます。

あとVec<T>pushpopはいらないけれどヒープに置きたいというときは、boxed slice(Box<[T]>)というものもあります。これはVec<T>から変換して作ります。

    #[bench]
    // Vec<T>
    fn vec(b: &mut Bencher) {
        let mut data = vec![0u8;32768*16];
        b.iter(move || {
            data.iter_mut().for_each(|x| *x += 1);
            // data.iter_mut().for_each(|x| *x += 1);
        });
    }

    #[bench]
    // Box<[T]>
    fn boxed_slice(b: &mut Bencher) {
        let mut data = vec![0u8;32768*16].into_boxed_slice(); // Box<[u8]>型
        b.iter(move || {
            data.iter_mut().for_each(|x| *x += 1);
            // data.iter_mut().for_each(|x| *x += 1);
        });
   }

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/02/27 20:51

    解答して頂き有難うございます。実際のコードではスタック領域を使い切ってしまう為、 Nightlyの機能なので好まれませんが`feature(box_syntax)`を使用しています。この場合はコピーを介さず作られるという事でしょうか?

    キャンセル

  • 2019/02/27 21:08

    あー、このBox::new()の振る舞いって以前からそうだった気がするので、そういう仕様だと思っていたのですが、こちらによるとバグらしいです。box syntaxを使ったときは意図した動作になって、コピーを介さず作られるそうです。https://github.com/rust-lang/rust/issues/58570

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.63%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る