避免使用std :: function和member函数进行内存分配

时间:2017-09-11 20:06:35

标签: c++ gcc

此代码仅用于说明问题。

#include <functional>
struct MyCallBack {
    void Fire() {
    }
};

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

使用valgrind进行的实验表明,分配给func的行在linux上使用gcc 7.1.1动态分配大约24个字节。

在实际代码中,我有一些不同的结构,所有结构都使用void(void)成员函数,存储在~1000万std::function<void(void)>中。

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

5 个答案:

答案 0 :(得分:20)

不幸的是,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之外,目前无法消除函数中的动态分配(显然,在一般情况下不推荐,但可以在某些特定情况下完成)。

答案 1 :(得分:5)

作为已经存在且正确答案的补充,请考虑以下事项:

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,只需要捕获指向一个对象的指针,也应该是一个单词,实际上应该永远不会分配。

答案 2 :(得分:1)

许多std :: function实现将避免分配并在函数类本身内部使用空间,而不是分配它所包含的回调是否足够小&#34;并且有琐碎的复制。但是,该标准并不要求这样,只建议它。

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

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

答案 3 :(得分:1)

我建议您根据具体用途使用自定义类。

虽然你不应该尝试重新实现现有的库功能,因为库的功能会更加经过测试和优化,但它也适用于< em>一般情况。如果您的示例中存在特定情况,并且标准实现不能满足您的需求,您可以探索实施针对您的特定用例量身定制的版本,您可以根据需要进行测量和调整。

所以我创建了一个类似于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();
}

警告:仅测试编译错误。

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

您需要对其进行分析,看看它是否具有比其他解决方案更好的性能。这样做的好处是因为它可以在实施中进行调整,以提高特定场景的性能。

答案 4 :(得分:0)

运行此小技巧,它可能会打印出无需分配内存即可捕获的字节数:

#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个字节时没有分配。
  捕获24个字节时调用了malloc。
  捕获32个字节时调用了malloc。