1. ホーム
  2. c++

[解決済み] C++11のラムダ実装とメモリモデル

2023-01-09 11:25:52

質問

C++11 のクロージャについて正しく考えるための情報が欲しいのですが、クロージャと std::function について、どのように実装され、どのようにメモリが処理されるかという観点から、正しく考える方法についての情報が欲しいです。

私は時期尚早な最適化は信じていませんが、新しいコードを書くときに、自分の選択がパフォーマンスに与える影響を注意深く考慮する習慣はあります。 また、マイクロコントローラーやオーディオシステムなど、非決定的なメモリの割り当て/解放の一時停止を回避するようなリアルタイムプログラミングをかなり行っています。

したがって、私は、C++ラムダをいつ使用するか、または使用しないかについて、より良い理解を深めたいと思います。

私の現在の理解は、キャプチャされたクロージャを持たないラムダは、まさにCのコールバックのようなものだということです。 しかし、環境が値または参照によってキャプチャされるとき、匿名オブジェクトがスタック上に作成されます。 値によるクロージャを関数から返さなければならない場合、それを std::function . この場合、クロージャのメモリはどうなるのでしょうか? スタックからヒープにコピーされるのでしょうか? クロージャメモリが解放されるのは std::function が解放されるたびに解放されるのでしょうか? std::shared_ptr ?

リアルタイムシステムにおいて、ラムダ関数のチェーンを設定し、Aへの継続引数としてBを渡すことで、処理パイプラインが A->B が作られる。 この場合、AとBのクロージャは一度確保されることになる。 これがスタックに確保されるのかヒープに確保されるのかはわからないが。 しかし、一般に、これはリアルタイム・システムで使用しても安全だと思われる。 一方、Bがラムダ関数Cを構築し、それを返す場合、Cのメモリは繰り返し確保・解放されることになり、リアルタイムでの使用には適さない。

疑似コードで、リアルタイムセーフになると思われるDSPのループを考えてみます。 処理ブロックAを実行し、次にBを実行したいのですが、Aはその引数を呼び出します。 これらの関数は両方とも std::function オブジェクトを返すので fstd::function オブジェクトとなり、その環境はヒープに保存されます。

auto f = A(B);  // A returns a function which calls B
                // Memory for the function returned by A is on the heap?
                // Note that A and B may maintain a state
                // via mutable value-closure!
for (t=0; t<1000; t++) {
    y = f(t)
}

そして、リアルタイムのコードで使うのはまずいかもしれないと思うもの。

for (t=0; t<1000; t++) {
    y = A(B)(t);
}

そして、スタックメモリがクロージャに使われている可能性が高いと思われるもの。

freq = 220;
A = 2;
for (t=0; t<1000; t++) {
    y = [=](int t){ return sin(t*freq)*A; }
}

後者の場合、クロージャはループの各反復で構築されますが、前の例とは異なり、関数呼び出しのようにヒープ割り当てが行われないため、安価になります。 さらに、コンパイラはクロージャを持ち上げ、インライン最適化を行うことができるのでしょうか。

これは正しいのでしょうか? ありがとうございます。

どのように解決するのですか?

<ブロッククオート

私の現在の理解では、キャプチャされたクロージャのないラムダは、C のコールバックとまったく同じです。しかし、環境が値または参照によってキャプチャされると、スタック上に匿名オブジェクトが作成されます。

いいえ、それは 常に であり、スタック上に生成された未知の型のC++オブジェクトです。キャプチャのないラムダは 変換される を関数ポインタに変換することができますが(ただし、C の呼び出し規約に適しているかどうかは実装に依存します)、だからといって 関数ポインタであることを意味するものではありません。

関数から値クロージャを返さなければならないとき、それをstd::functionでラップすることがあります。この場合、クロージャのメモリはどうなるのでしょうか?

C++11ではラムダは特別なものではなく、他のオブジェクトと同じようにオブジェクトです。ラムダ式の結果は一時的なもので、スタック上の変数を初期化するために使用することができます。

auto lamb = []() {return 5;};

lamb はスタックオブジェクトです。これはコンストラクタとデストラクタを持っています。そして、それに関するすべての C++ の規則に従います。の型は lamb の型には、取り込まれた値や参照が含まれます。それらは、他の型のオブジェクトのメンバと同じように、そのオブジェクトのメンバとなります。

に与えることができます。 std::function :

auto func_lamb = std::function<int()>(lamb);

この場合 コピー の値の lamb . もし lamb が何かを値で捕捉していた場合、それらの値のコピーが2つ存在することになります;1つは lamb に、もうひとつは func_lamb .

現在のスコープが終了したとき。 func_lamb は破棄され、その後に lamb が破壊され、スタック変数のクリーンアップのルールに従います。

ヒープ上のものを割り当てることも同様に簡単にできます。

auto func_lamb_ptr = new std::function<int()>(lamb);

の内容のメモリは、まさに std::function のメモリがどこにあるかは実装に依存します。 std::function が採用する型消去は一般に少なくとも1回のメモリ割り当てを必要とします。このため std::function のコンストラクタがアロケータを取ることができるのはこのためです。

std::function が解放されるたびに解放されるのか、つまり std::shared_ptr のように参照カウントされるのか?

std::function には コピー を格納します。事実上すべての標準ライブラリC++型と同様に function 値セマンティクス . したがって,コピー可能であり,コピーされた場合,新しい function オブジェクトは完全に分離されます。また、移動可能であるため、内部での割り当ては、さらに割り当てもコピーも必要なく、適切に転送することができます。

したがって、参照カウントは必要ありません。

あなたが述べる他のすべては、"メモリ割り当て"が"リアルタイムコードで使用するのが悪い"に等しいと仮定すると、正しいです。