104

C++11 クロージャーについて正しく考える方法とstd::function、クロージャーの実装方法とメモリの処理方法に関する情報が欲しいです。

私は時期尚早の最適化を信じていませんが、新しいコードを作成する際に、自分の選択がパフォーマンスに与える影響を慎重に検討する習慣があります。また、非決定論的なメモリ割り当て/割り当て解除の一時停止を避ける必要があるマイクロコントローラやオーディオ システムなど、かなりの量のリアルタイム プログラミングも行っています。

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

私の現在の理解では、キャプチャされたクロージャのないラムダは C コールバックとまったく同じです。ただし、環境が値または参照によってキャプチャされると、匿名オブジェクトがスタック上に作成されます。関数から value-closure を返さなければならないときは、それを でラップしstd::functionます。この場合、閉鎖記憶はどうなりますか? スタックからヒープにコピーされますか? が解放されるたびに解放されますか。std::functionつまり、 のように参照カウントされstd::shared_ptrますか?

リアルタイム システムでは、一連のラムダ関数を設定し、B を継続引数として A に渡し、処理パイプラインA->Bが作成されると想像します。この場合、A および B クロージャは 1 回割り当てられます。これらがスタックに割り当てられるかヒープに割り当てられるかはわかりませんが。ただし、一般に、これはリアルタイム システムで安全に使用できるようです。一方、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; }
}

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

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

4

2 に答える 2

111

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

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

関数から value-closure を返さなければならない場合、それを 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に 1 つfunc_lamb

現在のスコープが終了すると、スタック変数をクリーンアップする規則に従って、func_lambが破棄され、その後に が続きます。lamb

ヒープに簡単に割り当てることができます。

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

a の内容のメモリが正確にどこに移動するかstd::functionは実装に依存しますが、std::function一般に で使用される型消去には少なくとも 1 つのメモリ割り当てが必要です。std::functionこれが、のコンストラクターがアロケーターを取ることができる理由です。

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

std::functionその内容のコピーを保存します。ほとんどすべての標準ライブラリ C++ 型と同様に、値セマンティクスfunctionを使用します。したがって、コピー可能です。コピーすると、新しいオブジェクトは完全に分離されます。また、移動可能であるため、追加の割り当てやコピーを必要とせずに、内部割り当てを適切に転送できます。function

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

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

于 2012-08-30T18:43:19.033 に答える