Как избежать последовательных освобождений / выделений в C ++? - PullRequest
13 голосов
/ 02 февраля 2010

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

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.Что, IMO, безобразно.
  • В calledVeryOften() мне придется сделать что-то вроде этого: b = B(…), что неправильно по двум причинам:
    • Деструктор b не будет вызван.
    • Будет создан временный экземпляр B, затем скопирован в b, после чего будет вызван деструктор временного экземпляра.Копию и вызов деструктора можно избежать.Хуже того, вызов деструктора может привести к нежелательному поведению.

Итак, какие решения я должен избегать, используя new?Пожалуйста, имейте в виду, что:

  • У меня есть только контроль над A. У меня нет контроля над B, и у меня нет контроля над пользователями A.
  • Я хочу, чтобы код был максимально чистым и читабельным.

Ответы [ 10 ]

8 голосов
/ 02 февраля 2010

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

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

Он реализует все необходимые конструкторы, деструктор, копирование / присваивание, своп, ядда-ядда. Вот, пожалуйста:

#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's 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> и все готово. Никаких ручных выпусков или создания копировальных конструкторов, деструкторов и т. Д. Все позаботится в вашем маленьком, пригодном для повторного использования классе. :)

EDIT

Убрана необходимость в векторе, следует сэкономить немного места, а что-нет.

EDIT 2

Здесь используются aligned_storage и alignment_of для использования стека вместо кучи. Я использовал boost , но эта функциональность существует как в TR1, так и в C ++ 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's 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;
}

А вот и мы.

8 голосов
/ 02 февраля 2010

Просто зарезервируйте память, необходимую для 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;
    }

};

В некоторых случаях Pool или ObjectPool могут быть лучшей реализацией той же идеи.

Стоимость строительства / разрушения будет зависеть только от конструктора и деструктора класса B.

5 голосов
/ 02 февраля 2010

Как насчет выделения памяти для B один раз (или для самого большого возможного варианта) и использования размещения нового ?

A будет хранить char memB[sizeof(BiggestB)]; и B*.Конечно, вам придется вручную вызывать деструкторы, но никакая память не будет выделяться / освобождаться.

   void* p = memB;
   B* b = new(p) SomeB();
   ...
   b->~B();   // explicit destructor call when needed.
3 голосов
/ 03 февраля 2010

Быстрая проверка утверждения Мартина Йорка о том, что это преждевременная оптимизация, и что новые / удаляются оптимизируются гораздо дальше, чем просто программисты могут улучшить. Очевидно, что спрашивающий должен будет приурочить свой собственный код, чтобы увидеть, помогает ли ему избегать нового / удаления, но мне кажется, что для определенных классов и применений это будет иметь большое значение:

#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

В этом я не так уверен: теперь удаление / новый занимает в три раза больше времени, чем деструктор / размещение новой версии.

[Edit: я думаю, что я понял это - GCC 4 быстрее на 0-размерных векторах, фактически вычитая постоянное время из обеих версий кода. Изменение (a*b)%2 на (a*b)%2+1 восстанавливает соотношение времени 2: 1 с 3,7 с против 7,5]

Обратите внимание, что я не предпринял никаких специальных шагов для правильного выравнивания массива стека, но печать адреса показывает, что он выровнен по 16.

Кроме того, -g не влияет на время. Я оставил его случайно после того, как посмотрел на objdump, чтобы проверить, что -O3 не полностью удалил цикл. Эти указатели назывались yzz, потому что поиск «y» прошел не так хорошо, как я надеялся. Но я просто перезапущен без него.

3 голосов
/ 02 февраля 2010

Если 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;
}
1 голос
/ 02 февраля 2010

Как и другие уже предложили: попробуйте разместить новый ..

Вот полный пример:

#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);
}
1 голос
/ 02 февраля 2010

Вы уверены, что выделение памяти является узким местом, как вы думаете? Конструктор Б тривиально быстр?

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

Если типы и диапазоны параметра [1..4] являются разумными, а конструктор B - «тяжелым», вы также можете рассмотреть возможность использования кэшированного набора B. Это предполагает, что вам действительно разрешено иметь более одного в то время, когда он не находится перед ресурсом, например.

0 голосов
/ 03 февраля 2010

Просто возьмите кучу ранее использованных Б и используйте их снова.

0 голосов
/ 02 февраля 2010

Эмм, есть какая-то причина, по которой вы не можете сделать это?

A() : b(new B()) { }

void calledVeryOften(…) 
{
    b->setValues(param1, param2, param3, param4); 
}

(или установить их индивидуально, поскольку у вас нет доступа к классу B - эти значения у есть методы-мутаторы, верно?)

0 голосов
/ 02 февраля 2010

Я бы пошел с 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 .

...