如何避免C ++中的连续解除分配/分配?

时间:2010-02-02 20:04:31

标签: c++ oop optimization memory-management

请考虑以下代码:

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 个答案:

答案 0 :(得分:8)

只需保留b所需的内存(通过池或手动),并在每次删除/ new时重复使用,而不是每次重新分配。

示例:

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类的构造函数和析构函数。

答案 1 :(得分:8)

我喜欢Klaim的回答,所以我写得很快。我没有声称完美正确,但它对我来说看起来很不错。 (即,它唯一的测试是下面的样本main

这是一个通用的懒惰初始化程序。对象的空间分配一次,对象从null开始。然后,您可以create覆盖以前的对象,而无需新的内存分配。

它实现了所有必要的构造函数,析构函数,复制/赋值,交换,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'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>即可完成。没有手动发布或制作复制构造函数,析构函数等。一切都在你漂亮的小型可重用类中得到了解决。 :)

修改

删除了对vector的需要,应该节省一些空间和什么不是。

修改 2

这使用aligned_storagealignment_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;
}

我们走了。

答案 2 :(得分:5)

如何为B分配一次内存(或使用最大可能的变体)并使用placement new

A会存储char memB[sizeof(BiggestB)];B*。当然,您需要手动调用析构函数,但不会分配/取消分配内存。

   void* p = memB;
   B* b = new(p) SomeB();
   ...
   b->~B();   // explicit destructor call when needed.

答案 3 :(得分:3)

如果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;
}

答案 4 :(得分:3)

快速测试Martin York的断言,即这是一个过早的优化,并且新/删除的优化远远超出了仅仅是程序员改进的能力。显然,提问者必须计算他自己的代码,看看是否避免使用新的/删除帮助他,但在我看来,对于某些类和使用它会产生很大的不同:

#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

但是我不太确定:现在删除/新版本需要破坏/放置新版本的三倍。

[编辑:我想我已经弄明白了 - GCC 4在0大小的向量上更快,实际上从两个版本的代码中减去了一个恒定的时间。将(a*b)%2更改为(a*b)%2+1会恢复2:1的时间比率,而3.7s则为7.5]

请注意,我没有采取任何特殊步骤来正确对齐堆栈数组,但打印地址显示它是16对齐的。

此外,-g不会影响时间。在我看着objdump检查-O3没有完全移除循环后,我意外地离开了它。这个指针叫做yzz,因为搜索“y”并没有像我希望的那样好。但是我没有它就重新运行。

答案 5 :(得分:1)

您确定内存分配是您认为的瓶颈吗? B的构造函数是否很快?

如果内存分配是真正的问题,那么在这里放置新的或一些其他解决方案可能会有所帮助。

如果param [1..4]的类型和范围合理,并且B构造函数“重”,您可能还会考虑使用缓存的B集。这假设您实际上允许有多个例如,它不会以资源为前提。

答案 6 :(得分:1)

像其他人已经建议的那样:尝试安置新的..

这是一个完整的例子:

#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);
}

答案 7 :(得分:0)

我会在这里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是不可复制的,因为它应该在您的原始代码中,而不是在复制/赋值时泄漏内存。

如果您需要经常重新分配一些内部状态对象,我建议重新考虑设计。查看FlyweightState模式。

答案 8 :(得分:0)

呃,有什么理由你不能这样做吗?

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

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

(或单独设置,因为您无法访问B类 - 这些值执行有mutator-methods,对吗?)

答案 9 :(得分:0)

只需拥有一堆以前使用过的Bs,然后重复使用它们。