Избегайте выделения памяти с помощью функции 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
)
Ответы
Ответ 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 байтов.