Подтвердить что ты не робот

Избегайте выделения памяти с помощью функции std:: function и member

Этот код предназначен только для иллюстрации вопроса.

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

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

Эксперименты с valgrind показывают, что строка, назначающая func, динамически выделяет около 24 байтов с gcc 7.1.1 на linux.

В реальном коде у меня есть несколько горстей разных структур, все с функцией члена void(void), которая хранится в ~ 10 миллионов std::function<void(void)>.

Есть ли способ избежать динамического выделения памяти при выполнении std::function<void(void)> func = std::bind(&MyCallBack::Fire, &cb);? (Или иначе присвоить эту функцию-члену std::function)

4b9b3361

Ответ 1

К сожалению, распределители для std::function были сброшены в С++ 17.

Теперь принятое решение избежать динамических распределений внутри std::function заключается в использовании lambdas вместо std::bind. Это работает, по крайней мере, в GCC - у него достаточно статического пространства для хранения лямбда в вашем случае, но недостаточно места для хранения связующего объекта.

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

Как правило, с большинством реализаций и с лямбдой, которая захватывает только один указатель (или ссылку), вы избегаете динамических распределений внутри std::function с помощью этой методики (это также, как правило, лучший подход, как другой ответ предполагает).

Имейте в виду, для этого вам нужно гарантировать, что эта лямбда переживет std::function. Очевидно, что это не всегда возможно, и когда-нибудь вам нужно захватить состояние (большой) копией. Если это произойдет, то в настоящее время нет возможности устранить динамические распределения в функциях, кроме самозанятых с помощью STL (очевидно, не рекомендуется в общем случае, но может быть сделано в некоторых конкретных случаях).

Ответ 2

В качестве дополнения к уже существующему и правильному ответу, рассмотрите следующее:

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

Эта программа печатает 24 и 8 для меня, как с gcc, так и с clang. Я точно не знаю, что здесь делает bind (я понимаю, что это фантастически сложный зверь), но, как вы можете видеть, это почти абсурдно неэффективно здесь по сравнению с лямбдой.

Как это бывает, std::function гарантированно не выделяет, если он создан из указателя функции, который также является одним словом в размере. Итак, построим std::function из этого вида лямбда, которому нужно только захватить указатель на объект и также должно быть одно слово, на практике никогда не выделять.

Ответ 3

Многие реализации std:: function будут избегать выделения и использовать пространство внутри самого класса функции, а не выделять, если обратный вызов, который он обертывает, "достаточно мал" и имеет тривиальное копирование. Однако стандарт не требует этого, только предлагает его.

В g++ нетривиальный конструктор копирования для объекта функции или данных, превышающих 16 байтов, достаточно, чтобы заставить его выделить. Но если ваш объект функции не имеет данных и использует встроенный конструктор копий, то std:: function не будет выделять. Кроме того, если вы используете указатель на функцию или указатель на функцию-член, он не будет выделяться.

Пока вы не являетесь частью вашего вопроса, это часть вашего примера. Не используйте std:: bind. Практически в каждом случае лямбда лучше: меньшая, лучшая инкрустация, может избежать выделения, улучшать сообщения об ошибках, быстрее компилировать, список продолжается. Если вы хотите избежать выделения, вы также должны избегать связывания.

Ответ 4

Я предлагаю специальный класс для вашего конкретного использования.

Хотя верно, что вы не должны пытаться повторно реализовать существующую библиотечную функциональность, потому что библиотека будет намного более проверена и оптимизирована, также верно, что она применяется в общем случае. Если у вас есть такая ситуация, как в вашем примере, и стандартная реализация не отвечает вашим потребностям, вы можете изучить реализацию версии, адаптированной к вашему конкретному варианту использования, которую вы можете измерить и настроить по мере необходимости.

Итак, я создал класс, родственный 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();
}

Предупреждение: проверяется только на ошибки компиляции.

Вам необходимо тщательно проверить его на правильность.

Вам нужно просмотреть его и посмотреть, имеет ли он лучшую производительность, чем другие решения. Преимущество этого заключается в том, что в готовом доме вы можете внести изменения в реализацию, чтобы повысить производительность ваших конкретных сценариев.

Ответ 5

Запустите этот небольшой хак, и он, вероятно, выведет количество байтов, которое вы можете захватить без выделения памяти:

#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 байтов.