c++ 11 lambda实现和内存模型

时间:2022-06-17 21:05:26

I would like some information on how to correctly think about C++11 closures and std::function in terms of how they are implemented and how memory is handled.

我想要一些关于如何正确地考虑c++ 11闭包和std::函数的信息,它们是如何实现的,以及如何处理内存的。

Although I don't believe in premature optimisation, I do have a habit of carefully considering the performance impact of my choices while writing new code. I also do a fair amount of real-time programming, e.g. on microcontrollers and for audio systems, where non-deterministic memory allocation/deallocation pauses are to be avoided.

尽管我不相信过早的优化,但我有一个习惯,即在编写新代码时,仔细考虑我的选择对性能的影响。我还做了大量的实时编程,例如在微控制器和音频系统上,避免了不确定的内存分配/释放暂停。

Therefore I'd like to develop a better understanding of when to use or not use C++ lambdas.

因此,我希望更好地理解何时使用c++ lambdas。

My current understanding is that a lambda with no captured closure is exactly like a C callback. However, when the environment is captured either by value or by reference, an anonymous object is created on the stack. When a value-closure must be returned from a function, one wraps it in std::function. What happens to the closure memory in this case? Is it copied from the stack to the heap? Is it freed whenever the std::function is freed, i.e., is it reference-counted like a std::shared_ptr?

我目前的理解是,没有捕获闭包的lambda与C回调完全相同。但是,当环境通过值或引用捕获时,将在堆栈上创建一个匿名对象。当一个值闭包必须从一个函数中返回时,可以用std::function将它包装起来。在这种情况下,关闭内存发生了什么?它是从堆栈复制到堆吗?当std:::函数被释放时,它是否被释放?,它是否像std: shared_ptr那样进行引用计数?

I imagine that in a real-time system I could set up a chain of lambda functions, passing B as a continuation argument to A, so that a processing pipeline A->B is created. In this case, the A and B closures would be allocated once. Although I'm not sure whether these would be allocated on the stack or the heap. However in general this seems safe to use in a real-time system. On the other hand if B constructs some lambda function C, which it returns, then the memory for C would be allocated and deallocated repeatedly, which would not be acceptable for real-time usage.

假设在一个实时系统中,我可以建立一个lambda函数链,将B作为延续参数传递给a,这样就创建了一个处理管道a - >b。在这种情况下,将分配一次A和B闭包。尽管我不确定它们将被分配到堆栈还是堆上。然而,一般来说,这在实时系统中使用是安全的。另一方面,如果B构造了一些lambda函数C,它会返回,那么C的内存就会被多次分配和释放,这对于实时使用来说是不可接受的。

In pseudo-code, a DSP loop, which I think is going to be real-time safe. I want to perform processing block A and then B, where A calls its argument. Both these functions return std::function objects, so f will be a std::function object, where its environment is stored on the heap:

在伪代码中,DSP循环,我认为它是实时安全的。我要执行处理block A然后B,其中A调用它的参数。这两个函数都返回std::函数对象,所以f是std::函数对象,其环境存储在堆中:

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)
}

And one which I think might be bad to use in real-time code:

我认为在实时代码中使用它是不好的:

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

And one where I think stack memory is likely used for the closure:

我认为堆栈内存很可能用于闭包:

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

In the latter case the closure is constructed at each iteration of the loop, but unlike the previous example it is cheap because it is just like a function call, no heap allocations are made. Moreover, I wonder if a compiler could "lift" the closure and make inlining optimisations.

在后一种情况下,闭包是在循环的每次迭代中构造的,但是与前面的示例不同,闭包是廉价的,因为它就像一个函数调用,没有进行堆分配。此外,我想知道编译器是否能够“提升”闭包并进行内联优化。

Is this correct? Thank you.

这是正确的吗?谢谢你!

1 个解决方案

#1


80  

My current understanding is that a lambda with no captured closure is exactly like a C callback. However, when the environment is captured either by value or by reference, an anonymous object is created on the stack.

我目前的理解是,没有捕获闭包的lambda与C回调完全相同。但是,当环境通过值或引用捕获时,将在堆栈上创建一个匿名对象。

No; it is always a C++ object with an unknown type, created on the stack. A capture-less lambda can be converted into a function pointer (though whether it is suitable for C calling conventions is implementation dependent), but that doesn't mean it is a function pointer.

没有;它总是一个带有未知类型的c++对象,在堆栈上创建。无捕获lambda可以转换为函数指针(尽管它是否适用于C调用约定取决于实现),但这并不意味着它是函数指针。

When a value-closure must be returned from a function, one wraps it in std::function. What happens to the closure memory in this case?

当一个值闭包必须从一个函数中返回时,可以用std::function将它包装起来。在这种情况下,关闭内存发生了什么?

A lambda isn't anything special in C++11. It's an object like any other object. A lambda expression results in a temporary, which can be used to initialize a variable on the stack:

在c++ 11中,lambda没有什么特别之处。它和其他物体一样是一个物体。lambda表达式产生一个临时的,可用于初始化堆栈上的变量:

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

lamb is a stack object. It has a constructor and destructor. And it will follow all of the C++ rules for that. The type of lamb will contain the values/references that are captured; they will be members of that object, just like any other object members of any other type.

lamb是一个堆栈对象。它有构造函数和析构函数。它将遵循所有c++规则。羊肉的类型将包含捕获的值/引用;它们将是该对象的成员,就像任何其他类型的对象成员一样。

You can give it to a std::function:

你可以把它给std::

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

In this case, it will get a copy of the value of lamb. If lamb had captured anything by value, there would be two copies of those values; one in lamb, and one in func_lamb.

在这种情况下,它会得到一个lamb值的副本。如果兰博按值捕获任何东西,那么这些值将有两个副本;一个在羔羊身上,一个在仿羔羊身上。

When the current scope ends, func_lamb will be destroyed, followed by lamb, as per the rules of cleaning up stack variables.

当当前范围结束时,按照清理堆栈变量的规则,func_lamb和lamb一起被销毁。

You could just as easily allocate one on the heap:

您可以同样轻松地在堆上分配一个:

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

Exactly where the memory for the contents of a std::function goes is implementation-dependent, but the type-erasure employed by std::function generally requires at least one memory allocation. This is why std::function's constructor can take an allocator.

确切地说,std::函数内容的内存位于与实现相关的位置,但是std:::函数使用的类型擦除通常需要至少一个内存分配。这就是为什么std::函数的构造函数可以使用分配器。

Is it freed whenever the std::function is freed, i.e., is it reference-counted like a std::shared_ptr?

当std:::函数被释放时,它是否被释放?,它是否像std: shared_ptr那样进行引用计数?

std::function stores a copy of its contents. Like virtually every standard library C++ type, function uses value semantics. Thus, it is copyable; when it is copied, the new function object is completely separate. It is also moveable, so any internal allocations can be transferred appropriately without needing more allocating and copying.

函数的作用是:存储内容的副本。与几乎所有的标准库c++类型一样,函数使用值语义。因此,它是复制;当它被复制时,新的函数对象是完全独立的。它也是可移动的,因此任何内部分配都可以适当地转移,而不需要更多的分配和复制。

Thus there is no need for reference counting.

因此不需要引用计数。

Everything else you state is correct, assuming that "memory allocation" equates to "bad to use in real-time code".

假设“内存分配”等同于“无法在实时代码中使用”,那么您所陈述的其他内容都是正确的。

#1


80  

My current understanding is that a lambda with no captured closure is exactly like a C callback. However, when the environment is captured either by value or by reference, an anonymous object is created on the stack.

我目前的理解是,没有捕获闭包的lambda与C回调完全相同。但是,当环境通过值或引用捕获时,将在堆栈上创建一个匿名对象。

No; it is always a C++ object with an unknown type, created on the stack. A capture-less lambda can be converted into a function pointer (though whether it is suitable for C calling conventions is implementation dependent), but that doesn't mean it is a function pointer.

没有;它总是一个带有未知类型的c++对象,在堆栈上创建。无捕获lambda可以转换为函数指针(尽管它是否适用于C调用约定取决于实现),但这并不意味着它是函数指针。

When a value-closure must be returned from a function, one wraps it in std::function. What happens to the closure memory in this case?

当一个值闭包必须从一个函数中返回时,可以用std::function将它包装起来。在这种情况下,关闭内存发生了什么?

A lambda isn't anything special in C++11. It's an object like any other object. A lambda expression results in a temporary, which can be used to initialize a variable on the stack:

在c++ 11中,lambda没有什么特别之处。它和其他物体一样是一个物体。lambda表达式产生一个临时的,可用于初始化堆栈上的变量:

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

lamb is a stack object. It has a constructor and destructor. And it will follow all of the C++ rules for that. The type of lamb will contain the values/references that are captured; they will be members of that object, just like any other object members of any other type.

lamb是一个堆栈对象。它有构造函数和析构函数。它将遵循所有c++规则。羊肉的类型将包含捕获的值/引用;它们将是该对象的成员,就像任何其他类型的对象成员一样。

You can give it to a std::function:

你可以把它给std::

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

In this case, it will get a copy of the value of lamb. If lamb had captured anything by value, there would be two copies of those values; one in lamb, and one in func_lamb.

在这种情况下,它会得到一个lamb值的副本。如果兰博按值捕获任何东西,那么这些值将有两个副本;一个在羔羊身上,一个在仿羔羊身上。

When the current scope ends, func_lamb will be destroyed, followed by lamb, as per the rules of cleaning up stack variables.

当当前范围结束时,按照清理堆栈变量的规则,func_lamb和lamb一起被销毁。

You could just as easily allocate one on the heap:

您可以同样轻松地在堆上分配一个:

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

Exactly where the memory for the contents of a std::function goes is implementation-dependent, but the type-erasure employed by std::function generally requires at least one memory allocation. This is why std::function's constructor can take an allocator.

确切地说,std::函数内容的内存位于与实现相关的位置,但是std:::函数使用的类型擦除通常需要至少一个内存分配。这就是为什么std::函数的构造函数可以使用分配器。

Is it freed whenever the std::function is freed, i.e., is it reference-counted like a std::shared_ptr?

当std:::函数被释放时,它是否被释放?,它是否像std: shared_ptr那样进行引用计数?

std::function stores a copy of its contents. Like virtually every standard library C++ type, function uses value semantics. Thus, it is copyable; when it is copied, the new function object is completely separate. It is also moveable, so any internal allocations can be transferred appropriately without needing more allocating and copying.

函数的作用是:存储内容的副本。与几乎所有的标准库c++类型一样,函数使用值语义。因此,它是复制;当它被复制时,新的函数对象是完全独立的。它也是可移动的,因此任何内部分配都可以适当地转移,而不需要更多的分配和复制。

Thus there is no need for reference counting.

因此不需要引用计数。

Everything else you state is correct, assuming that "memory allocation" equates to "bad to use in real-time code".

假设“内存分配”等同于“无法在实时代码中使用”,那么您所陈述的其他内容都是正确的。