c++ - sistemas - Evite la asignación de memoria con la función std:: y la función miembro
paginacion de memoria ejemplos (4)
Como una adición a la respuesta ya existente y correcta, considere lo siguiente:
MyCallBack cb;
std::cerr << sizeof(std::bind(&MyCallBack::Fire, &cb)) << "/n";
auto a = [&] { cb.Fire(); };
std::cerr << sizeof(a);
Este programa imprime 24 y 8 para mí, con gcc y clang. No sé exactamente qué es lo que está haciendo aquí (mi entendimiento es que es una bestia fantásticamente complicada), pero como pueden ver, es absurdamente ineficiente aquí en comparación con una lambda.
Da la casualidad, se garantiza que std::function
no se asignará si se construye a partir de un puntero a función, que también tiene una palabra en tamaño. Por lo tanto, construir una std::function
partir de este tipo de lambda, que solo necesita capturar un puntero a un objeto y también debe ser una palabra, en la práctica, nunca debe asignarse.
Este código es solo para ilustrar la pregunta.
#include <functional>
struct MyCallBack {
void Fire() {
}
};
int main()
{
MyCallBack cb;
std::function<void(void)> func = std::bind(&MyCallBack::Fire, &cb);
}
Los experimentos con valgrind muestran que la línea que asigna a func
asigna dinámicamente unos 24 bytes con gcc 7.1.1 en Linux.
En el código real, tengo algunas pocas estructuras diferentes, todas con una función de miembro void(void)
que se almacena en ~ 10 millones std::function<void(void)>
.
¿Hay alguna manera de evitar que la memoria se asigne dinámicamente al hacer std::function<void(void)> func = std::bind(&MyCallBack::Fire, &cb);
? (O bien, asigne esta función miembro a una std::function
Muchas implementaciones de std :: function evitarán las asignaciones y usarán el espacio dentro de la clase de función en lugar de asignar si la devolución de llamada que envuelve es "lo suficientemente pequeña" y tiene una copia trivial. Sin embargo, el estándar no requiere esto, solo lo sugiere.
En g ++, un constructor de copia no trivial en un objeto de función, o datos que exceden 16 bytes, es suficiente para hacer que se asigne. Pero si su objeto de función no tiene datos y usa el constructor de copia integrada, entonces std :: function no asignará. Además, si usa un puntero de función o un puntero de función miembro, no asignará.
Si bien no forma parte directamente de su pregunta, es parte de su ejemplo. No use std :: bind. En casi todos los casos, una lambda es mejor: más pequeña, mejor alineación, puede evitar asignaciones, mejores mensajes de error, compilaciones más rápidas, la lista continúa. Si quiere evitar asignaciones, también debe evitar bind.
Desafortunadamente, los asignadores para std::function
se han eliminado en C ++ 17.
Ahora la solución aceptada para evitar asignaciones dinámicas dentro de std::function
es usar lambdas en lugar de std::bind
. Eso funciona, al menos en GCC: tiene suficiente espacio estático para almacenar el lambda en su caja, pero no espacio suficiente para almacenar el objeto de encuadernación.
std::function<void()> func = [&cb]{ cb.Fire(); };
// sizeof lambda is sizeof(MyCallBack*), which is small enough
Como regla general, con la mayoría de las implementaciones, y con una lambda que captura solo un puntero (o una referencia), evitará las asignaciones dinámicas dentro de std::function
con esta técnica (generalmente también es mejor enfoque como sugiere otra respuesta) .
Tenga en cuenta que, para que funcione, es necesario garantizar que esta lambda sobrevivirá a la std::function
. Obviamente, no siempre es posible, y en algún momento debe capturar el estado mediante una copia (grande). Si eso sucede, no hay manera de eliminar las asignaciones dinámicas en funciones, aparte de jugar con AWL usted mismo (obviamente, no se recomienda en el caso general, pero podría hacerse en algunos casos específicos).
Propongo una clase personalizada para su uso específico.
Si bien es cierto que no debe intentar volver a implementar la funcionalidad de la biblioteca existente porque la biblioteca será mucho más probada y optimizada, también es cierto que se aplica para el caso general . Si tiene una situación particular como en su ejemplo y la implementación estándar no satisface sus necesidades, puede explorar la implementación de una versión adaptada a su caso de uso específico, que puede medir y modificar según sea necesario.
Así que he creado una clase similar a std::function<void (void)>
que funciona solo para métodos y tiene todo el almacenamiento en su lugar (sin asignaciones dinámicas).
Lo llamé amorosamente Trigger
(inspirado en el nombre de tu método Fire
). Por favor, déle un nombre más adecuado si lo desea.
// 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();
}
};
Uso:
struct X
{
auto foo() -> void;
};
auto test()
{
X x;
Trigger f{x, &X::foo};
f();
}
Advertencia: solo probado para errores de compilación.
Debes probarlo minuciosamente para verificar que sea correcto.
Necesita perfilarlo y ver si tiene un mejor rendimiento que otras soluciones. La ventaja de esto es que, como está hecho en casa, puede realizar ajustes en la implementación para aumentar el rendimiento en sus escenarios específicos.