Как избежать последовательных освобождений/распределений в С++?
Рассмотрим следующий код:
class A
{
B* b; // an A object owns a B object
A() : b(NULL) { } // we don't know what b will be when constructing A
void calledVeryOften(…)
{
if (b)
delete b;
b = new B(param1, param2, param3, param4);
}
};
Моя цель: мне нужно максимизировать производительность, которая в этом случае означает минимизацию объема выделения памяти.
Очевидная вещь здесь - изменить B* b;
на B b;
. Я вижу две проблемы с этим подходом:
- Мне нужно инициализировать
b
в конструкторе. Поскольку я не знаю, что будет b
, это означает, что мне нужно передать фиктивные значения в конструктор B. Который, ИМО, уродлив.
- В
calledVeryOften()
мне нужно будет сделать что-то вроде этого: b = B(…)
, что неверно по двум причинам:
- Деструктор
b
не будет вызываться.
- Будет создан временный экземпляр B, а затем скопирован в
b
, тогда будет вызван деструктор временного экземпляра. Копирование и вызов деструктора можно было бы избежать. Хуже того, вызов деструктора вполне может привести к нежелательному поведению.
Итак, какие решения мне нужно избегать, используя new
? Имейте в виду, что:
- У меня есть только контроль над A. У меня нет контроля над B, и у меня нет контроля над пользователями A.
- Я хочу, чтобы код был чистым и удобным для чтения.
Ответы
Ответ 1
Мне понравился ответ Кэйдж, поэтому я написал это очень быстро. Я не претендую на совершенную правильность, но это выглядит очень хорошо для меня. (т.е. единственное его тестирование - образец main
ниже)
Это общий ленивый инициализатор. Пространство для объекта выделяется один раз, и объект начинается с нуля. Затем вы можете create
перезаписать предыдущие объекты без новых распределений памяти.
Он реализует все необходимые конструкторы, деструктор, копию/назначение, swap, yadda-yadda. Вот вы:
#include <cassert>
#include <new>
template <typename T>
class lazy_object
{
public:
// types
typedef T value_type;
typedef const T const_value_type;
typedef value_type& reference;
typedef const_value_type& const_reference;
typedef value_type* pointer;
typedef const_value_type* const_pointer;
// creation
lazy_object(void) :
mObject(0),
mBuffer(::operator new(sizeof(T)))
{
}
lazy_object(const lazy_object& pRhs) :
mObject(0),
mBuffer(::operator new(sizeof(T)))
{
if (pRhs.exists())
{
mObject = new (buffer()) T(pRhs.get());
}
}
lazy_object& operator=(lazy_object pRhs)
{
pRhs.swap(*this);
return *this;
}
~lazy_object(void)
{
destroy();
::operator delete(mBuffer);
}
// need to make multiple versions of this.
// variadic templates/Boost.PreProccesor
// would help immensely. For now, I give
// two, but it easy to make more.
void create(void)
{
destroy();
mObject = new (buffer()) T();
}
template <typename A1>
void create(const A1 pA1)
{
destroy();
mObject = new (buffer()) T(pA1);
}
void destroy(void)
{
if (exists())
{
mObject->~T();
mObject = 0;
}
}
void swap(lazy_object& pRhs)
{
std::swap(mObject, pRhs.mObject);
std::swap(mBuffer, pRhs.mBuffer);
}
// access
reference get(void)
{
return *get_ptr();
}
const_reference get(void) const
{
return *get_ptr();
}
pointer get_ptr(void)
{
assert(exists());
return mObject;
}
const_pointer get_ptr(void) const
{
assert(exists());
return mObject;
}
void* buffer(void)
{
return mBuffer;
}
// query
const bool exists(void) const
{
return mObject != 0;
}
private:
// members
pointer mObject;
void* mBuffer;
};
// explicit swaps for generality
template <typename T>
void swap(lazy_object<T>& pLhs, lazy_object<T>& pRhs)
{
pLhs.swap(pRhs);
}
// if the above code is in a namespace, don't put this in it!
// specializations in global namespace std are allowed.
namespace std
{
template <typename T>
void swap(lazy_object<T>& pLhs, lazy_object<T>& pRhs)
{
pLhs.swap(pRhs);
}
}
// test use
#include <iostream>
int main(void)
{
// basic usage
lazy_object<int> i;
i.create();
i.get() = 5;
std::cout << i.get() << std::endl;
// asserts (not created yet)
lazy_object<double> d;
std::cout << d.get() << std::endl;
}
В вашем случае просто создайте член в своем классе: lazy_object<B>
, и все готово. Никаких ручных релизов или создания копий-конструкторов, деструкторов и т.д. Все позаботится о вашем хорошем, небольшом классе повторного использования.:)
ИЗМЕНИТЬ
Убрал необходимость в векторе, должен сохранить немного места и что-нет.
ИЗМЕНИТЬ 2
Для использования стека вместо кучи используется aligned_storage
и alignment_of
. Я использовал boost, но эта функциональность существует как в TR1, так и в С++ 0x. Мы теряем возможность копировать и, следовательно, свопируем.
#include <boost/type_traits/aligned_storage.hpp>
#include <cassert>
#include <new>
template <typename T>
class lazy_object_stack
{
public:
// types
typedef T value_type;
typedef const T const_value_type;
typedef value_type& reference;
typedef const_value_type& const_reference;
typedef value_type* pointer;
typedef const_value_type* const_pointer;
// creation
lazy_object_stack(void) :
mObject(0)
{
}
~lazy_object_stack(void)
{
destroy();
}
// need to make multiple versions of this.
// variadic templates/Boost.PreProccesor
// would help immensely. For now, I give
// two, but it easy to make more.
void create(void)
{
destroy();
mObject = new (buffer()) T();
}
template <typename A1>
void create(const A1 pA1)
{
destroy();
mObject = new (buffer()) T(pA1);
}
void destroy(void)
{
if (exists())
{
mObject->~T();
mObject = 0;
}
}
// access
reference get(void)
{
return *get_ptr();
}
const_reference get(void) const
{
return *get_ptr();
}
pointer get_ptr(void)
{
assert(exists());
return mObject;
}
const_pointer get_ptr(void) const
{
assert(exists());
return mObject;
}
void* buffer(void)
{
return mBuffer.address();
}
// query
const bool exists(void) const
{
return mObject != 0;
}
private:
// types
typedef boost::aligned_storage<sizeof(T),
boost::alignment_of<T>::value> storage_type;
// members
pointer mObject;
storage_type mBuffer;
// non-copyable
lazy_object_stack(const lazy_object_stack& pRhs);
lazy_object_stack& operator=(lazy_object_stack pRhs);
};
// test use
#include <iostream>
int main(void)
{
// basic usage
lazy_object_stack<int> i;
i.create();
i.get() = 5;
std::cout << i.get() << std::endl;
// asserts (not created yet)
lazy_object_stack<double> d;
std::cout << d.get() << std::endl;
}
И вот мы идем.
Ответ 2
Просто зарезервируйте требуемую память для b (через пул или вручную) и повторно используйте его каждый раз, когда вы удаляете/новое, а не перераспределяете каждый раз.
Пример:
class A
{
B* b; // an A object owns a B object
bool initialized;
public:
A() : b( malloc( sizeof(B) ) ), initialized(false) { } // We reserve memory for b
~A() { if(initialized) destroy(); free(b); } // release memory only once we don't use it anymore
void calledVeryOften(…)
{
if (initialized)
destroy();
create();
}
private:
void destroy() { b->~B(); initialized = false; } // hand call to the destructor
void create( param1, param2, param3, param4 )
{
b = new (b) B( param1, param2, param3, param4 ); // in place new : only construct, don't allocate but use the memory that the provided pointer point to
initialized = true;
}
};
В некоторых случаях пул или ObjectPool может быть лучшей реализацией той же идеи.
Стоимость строительства/уничтожения будет только зависеть от конструктора и деструктора класса B.
Ответ 3
Как насчет выделения памяти для B один раз (или для нее самый большой возможный вариант) и используя размещение нового?
A сохранит char memB[sizeof(BiggestB)];
и B*
. Конечно, вам нужно будет вручную вызвать деструкторов, но память не будет выделена/освобождена.
void* p = memB;
B* b = new(p) SomeB();
...
b->~B(); // explicit destructor call when needed.
Ответ 4
Если B
правильно реализует свой оператор присваивания копий, то b = B(...)
не должен вызывать деструктор на B
. Это наиболее очевидное решение вашей проблемы.
Если, однако, B
не может быть соответствующим образом инициализирован по умолчанию, вы можете сделать что-то вроде этого. Я бы рекомендовал этот подход в качестве крайней меры, так как это очень трудно получить в безопасности. Непроверенный и, вероятно, с ошибками исключения в случае исключения из окна:
// Used to clean up raw memory of construction of B fails
struct PlacementHelper
{
PlacementHelper() : placement(NULL)
{
}
~PlacementHelper()
{
operator delete(placement);
}
void* placement;
};
void calledVeryOften(....)
{
PlacementHelper hp;
if (b == NULL)
{
hp.placement = operator new(sizeof(B));
}
else
{
hp.placement = b;
b->~B();
b = NULL; // We can't let b be non-null but point at an invalid B
}
// If construction throws, hp will clean up the raw memory
b = new (placement) B(param1, param2, param3, param4);
// Stop hp from cleaning up; b points at a valid object
hp.placement = NULL;
}
Ответ 5
Быстрая проверка утверждения Мартина Йорка о том, что это преждевременная оптимизация, и что новые/удаленные оптимизируются далеко за пределы возможностей простых программистов. Очевидно, собеседник должен будет разузнать свой собственный код, чтобы узнать, помогает ли ему избежать использования new/delete, но мне кажется, что для определенных классов и применений это будет иметь большое значение:
#include <iostream>
#include <vector>
int g_construct = 0;
int g_destruct = 0;
struct A {
std::vector<int> vec;
A (int a, int b) : vec((a*b) % 2) { ++g_construct; }
~A() {
++g_destruct;
}
};
int main() {
const int times = 10*1000*1000;
#if DYNAMIC
std::cout << "dynamic\n";
A *x = new A(1,3);
for (int i = 0; i < times; ++i) {
delete x;
x = new A(i,3);
}
#else
std::cout << "automatic\n";
char x[sizeof(A)];
A* yzz = new (x) A(1,3);
for (int i = 0; i < times; ++i) {
yzz->~A();
new (x) A(i,3);
}
#endif
std::cout << g_construct << " constructors and " << g_destruct << " destructors\n";
}
$ g++ allocperf.cpp -oallocperf -O3 -DDYNAMIC=0 -g && time ./allocperf
automatic
10000001 constructors and 10000000 destructors
real 0m7.718s
user 0m7.671s
sys 0m0.030s
$ g++ allocperf.cpp -oallocperf -O3 -DDYNAMIC=1 -g && time ./allocperf
dynamic
10000001 constructors and 10000000 destructors
real 0m15.188s
user 0m15.077s
sys 0m0.047s
Это примерно то, что я ожидал: код GMan-стиля (деструкция/размещение нового) занимает в два раза больше и, по-видимому, делает в два раза больше распределения. Если векторный элемент A заменен на int, тогда код типа GMan занимает часть секунды. Это GCC 3.
$ g++-4 allocperf.cpp -oallocperf -O3 -DDYNAMIC=1 -g && time ./allocperf
dynamic
10000001 constructors and 10000000 destructors
real 0m5.969s
user 0m5.905s
sys 0m0.030s
$ g++-4 allocperf.cpp -oallocperf -O3 -DDYNAMIC=0 -g && time ./allocperf
automatic
10000001 constructors and 10000000 destructors
real 0m2.047s
user 0m1.983s
sys 0m0.000s
Я не уверен в этом: теперь удаление/новое занимает в три раза больше, чем новая версия destruct/placement.
[Edit: Я думаю, что я понял это - GCC 4 быстрее на векторах размера 0, фактически вычитая постоянное время из обеих версий кода. Изменение (a*b)%2
на (a*b)%2+1
восстанавливает соотношение времени 2: 1, с 3.7s против 7.5]
Обратите внимание, что я не предпринял никаких специальных шагов, чтобы правильно выровнять массив стека, но печать адреса показывает, что он выровнен по 16.
Кроме того, -g не влияет на тайминги. Я случайно оставил его после того, как посмотрел на objdump, чтобы проверить, что -O3 не полностью удалил цикл. Эти указатели называли yzz, потому что поиск "y" не шел так же хорошо, как я надеялся. Но я только что перезапустил его.
Ответ 6
Вы уверены, что выделение памяти является узким местом, которое, по вашему мнению, является? Является ли конструктор B тривиально быстрым?
Если распределение памяти является реальной проблемой, то здесь могут помочь размещение новых или некоторых других решений.
Если типы и диапазоны параметра [1..4] являются разумными, а конструктор B "тяжелый", вы также можете рассмотреть использование кэшированного набора B. Это предполагает, что вам фактически разрешено иметь более одного в то время, что он не запускает ресурс, например.
Ответ 7
Как и другие, уже предлагаемые: Попробуйте разместить новое..
Вот полный пример:
#include <new>
#include <stdio.h>
class B
{
public:
int dummy;
B (int arg)
{
dummy = arg;
printf ("C'Tor called\n");
}
~B ()
{
printf ("D'tor called\n");
}
};
void called_often (B * arg)
{
// call D'tor without freeing memory:
arg->~B();
// call C'tor without allocating memory:
arg = new(arg) B(10);
}
int main (int argc, char **args)
{
B test(1);
called_often (&test);
}
Ответ 8
Я бы пошел с boost:: scoped_ptr здесь:
class A: boost::noncopyable
{
typedef boost::scoped_ptr<B> b_ptr;
b_ptr pb_;
public:
A() : pb_() {}
void calledVeryOften( /*…*/ )
{
pb_.reset( new B( params )); // old instance deallocated
// safely use *pb_ as reference to instance of B
}
};
Нет необходимости в деструкторе, создаваемом вручную, A
не копируется, как и должно быть в вашем исходном коде, чтобы не утечка памяти при копировании/присваивании.
Я бы предложил подумать о дизайне, хотя вам нужно очень часто переназначать какой-то объект внутреннего состояния. Посмотрите Flyweight и State шаблоны.
Ответ 9
Erm, есть ли причина, по которой вы не можете это сделать?
A() : b(new B()) { }
void calledVeryOften(…)
{
b->setValues(param1, param2, param3, param4);
}
(или установить их отдельно, так как у вас нет доступа к классу B
- эти значения do имеют методы-мутаторы, верно?)
Ответ 10
Просто купите ранее использованные Bs и повторно используйте их.