Как избежать последовательных освобождений/распределений в С++?

Рассмотрим следующий код:

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 и повторно используйте их.