避免使用std::函数和成员函数进行内存分配

Avoid memory allocation with std::function and member function

本文关键字:函数 内存 分配 成员 std      更新时间:2023-10-16

这段代码只是为了说明这个问题。

#include <functional>
struct MyCallBack {
void Fire() {
}
};
int main()
{
MyCallBack cb;
std::function<void(void)> func = std::bind(&MyCallBack::Fire, &cb);
}

用valgrind进行的实验表明,在linux上,用gcc7.1.1为func动态分配了大约24个字节。

在实际的代码中,我有几把不同的结构,它们都有一个void(void)成员函数,存储在大约1000万个std::function<void(void)>中。

在执行std::function<void(void)> func = std::bind(&MyCallBack::Fire, &cb);时,有什么方法可以避免内存被动态分配吗?(或者将这些成员函数分配给std::function)

不幸的是,std::function的分配器已在C++17中删除。

现在,避免std::function内部动态分配的公认解决方案是使用lambdas而不是std::bind。这确实有效,至少在GCC中是这样——它有足够的静态空间来存储您的情况下的lambda,但没有足够的空间来存储binder对象。

std::function<void()> func = [&cb]{ cb.Fire(); };
// sizeof lambda is sizeof(MyCallBack*), which is small enough

一般来说,对于大多数实现,以及只捕获单个指针(或引用)的lambda,使用此技术可以避免std::function内部的动态分配(正如其他答案所示,这通常也是更好的方法)。

请记住,要实现这一点,您需要保证此lambda将比std::function更长寿。显然,这并不总是可能的,有时您必须逐个(大)拷贝捕获状态。如果发生这种情况,除了自己修改STL之外,目前没有办法消除函数中的动态分配(显然,在一般情况下不推荐,但在某些特定情况下可以这样做)。

作为已经存在的正确答案的附录,请考虑以下内容:

MyCallBack cb;
std::cerr << sizeof(std::bind(&MyCallBack::Fire, &cb)) << "n";
auto a = [&] { cb.Fire(); };
std::cerr << sizeof(a);

这个程序为我打印24和8,同时带有gcc和clang。我不知道bind在这里做什么(我的理解是它是一个极其复杂的野兽),但正如你所看到的,与lambda相比,它在这里的效率几乎低得离谱。

碰巧的是,如果std::function是从函数指针构建的,那么它保证不会分配,函数指针的大小也是一个字。因此,从这种lambda构造std::function,它只需要捕获一个指向对象的指针,而且应该是一个单词,在实践中应该永远不会分配。

运行这个小破解,它可能会打印出您可以在不分配内存的情况下捕获的字节数:

#include <iostream>
#include <functional>
#include <cstring>
void h(std::function<void(void*)>&& f, void* g)
{
f(g);
}
template<size_t number_of_size_t>
void do_test()
{
size_t a[number_of_size_t];
std::memset(a, 0, sizeof(a));
a[0] = sizeof(a);
std::function<void(void*)> g = [a](void* ptr) {
if (&a != ptr)
std::cout << "malloc was called when capturing " << a[0] << " bytes." << std::endl;
else
std::cout << "No allocation took place when capturing " << a[0] << " bytes." << std::endl;
};
h(std::move(g), &g);
}
int main()
{
do_test<1>();
do_test<2>();
do_test<3>();
do_test<4>();
}

使用gcc version 8.3.0打印

捕获8个字节时未进行分配
捕获16个字节时未进行分配
malloc是在捕获24个字节时调用的
malloc是在捕获32个字节时调用的。

许多std::函数实现将避免分配,并使用函数类本身内部的空间,而不是在其封装的回调"足够小"且具有琐碎的复制时进行分配。然而,该标准并不要求这样做,只是建议这样做

在g++上,函数对象上的一个非平凡的复制构造函数,或超过16字节的数据,足以使其进行分配。但是,如果您的函数对象没有数据,并且使用内置的复制构造函数,那么std::function将不会进行分配。此外,如果您使用函数指针或成员函数指针,它将不会进行分配。

虽然不是你问题的直接组成部分,但它是你例子的一部分。不要使用std::bind。在几乎所有情况下,lambda都更好:更小、更好的内联、可以避免分配、更好的错误消息、更快的编译,列表还在继续。如果你想避免分配,你还必须避免绑定。

我为您的特定用途提出了一个自定义类。

虽然确实不应该尝试重新实现现有的库功能,因为库功能将经过更多的测试和优化,但它也适用于一般情况。如果你有一个特殊的情况,比如你的例子,而标准实现不适合你的需求,你可以探索实现一个适合你的特定用例的版本,你可以根据需要进行测量和调整。

因此,我创建了一个类似于std::function<void (void)>的类,它只适用于方法,并且具有所有的存储(没有动态分配)。

我亲切地称它为Trigger(灵感来自您的Fire方法名称)。如果你想的话,请给它一个更合适的名字。

// helper alias for method
// can be used in user code
template <class T>
using Trigger_method = auto (T::*)() -> void;
namespace detail
{
// Polymorphic classes needed for type erasure
struct Trigger_base
{
virtual ~Trigger_base() noexcept = default;
virtual auto placement_clone(void* buffer) const noexcept -> Trigger_base* = 0;
virtual auto call() -> void = 0;
};
template <class T>
struct Trigger_actual : Trigger_base
{
T& obj;
Trigger_method<T> method;
Trigger_actual(T& obj, Trigger_method<T> method) noexcept : obj{obj}, method{method}
{
}
auto placement_clone(void* buffer) const noexcept -> Trigger_base* override
{
return new (buffer) Trigger_actual{obj, method};
}
auto call() -> void override
{
return (obj.*method)();
}
};
// in Trigger (bellow) we need to allocate enough storage
// for any Trigger_actual template instantiation
// since all templates basically contain 2 pointers
// we assume (and test it with static_asserts)
// that all will have the same size
// we will use Trigger_actual<Trigger_test_size>
// to determine the size of all Trigger_actual templates
struct Trigger_test_size {};
}
struct Trigger
{
std::aligned_storage_t<sizeof(detail::Trigger_actual<detail::Trigger_test_size>)>
trigger_actual_storage_;
// vital. We cannot just cast `&trigger_actual_storage_` to `Trigger_base*`
// because there is no guarantee by the standard that
// the base pointer will point to the start of the derived object
// so we need to store separately  the base pointer
detail::Trigger_base* base_ptr = nullptr;
template <class X>
Trigger(X& x, Trigger_method<X> method) noexcept
{
static_assert(sizeof(trigger_actual_storage_) >= 
sizeof(detail::Trigger_actual<X>));
static_assert(alignof(decltype(trigger_actual_storage_)) %
alignof(detail::Trigger_actual<X>) == 0);
base_ptr = new (&trigger_actual_storage_) detail::Trigger_actual<X>{x, method};
}
Trigger(const Trigger& other) noexcept
{
if (other.base_ptr)
{
base_ptr = other.base_ptr->placement_clone(&trigger_actual_storage_);
}
}
auto operator=(const Trigger& other) noexcept -> Trigger&
{
destroy_actual();
if (other.base_ptr)
{
base_ptr = other.base_ptr->placement_clone(&trigger_actual_storage_);
}
return *this;
}
~Trigger() noexcept
{
destroy_actual();
}
auto destroy_actual() noexcept -> void
{
if (base_ptr)
{
base_ptr->~Trigger_base();
base_ptr = nullptr;
}
}
auto operator()() const
{
if (!base_ptr)
{
// deal with this situation (error or just ignore and return)
}
base_ptr->call();
}
};

用法:

struct X
{    
auto foo() -> void;
};

auto test()
{
X x;
Trigger f{x, &X::foo};
f();
}

警告:仅针对编译错误进行了测试。

你需要彻底测试它的正确性。

您需要对其进行评测,看看它是否比其他解决方案具有更好的性能。这样做的好处是,它是内部准备的,您可以对实现进行调整,以提高特定场景的性能。

正如@Quxtplusone在他们的answer-As-comment中提到的那样,你可以在这里使用inplace_function。在你的项目中包括标题,然后像这样使用:

#include "inplace_function.h"
struct big { char foo[20]; };
static stdext::inplace_function<void(), 8> inplacefunc;
static std::function<void()> stdfunc;
int main() {
static_assert(sizeof(inplacefunc) == 16);
static_assert(sizeof(stdfunc) == 32);
inplacefunc = []() {};
// fine
struct big a;
inplacefunc = [a]() {};
// test.cpp:15:24:   required from here
// inplace_function.h:237:33: error: static assertion failed: inplace_function cannot be constructed from object with this (large) size
//  237 |         static_assert(sizeof(C) <= Capacity,
//      |                       ~~~~~~~~~~^~~~~~~~~~~
// inplace_function.h:237:33: note: the comparison reduces to ‘(20 <= 8)’
}