【问题标题】:How to avoid successive deallocations/allocations in C++?如何避免 C++ 中的连续释放/分配?
【发布时间】:2011-01-12 08:02:42
【问题描述】:

考虑以下代码:

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 的用户。
  • 我希望尽可能保持代码简洁易读。

【问题讨论】:

  • 附注,delete b; 之前不需要if(b)
  • B没有对应的函数:setParam1()、setParam2()等?
  • 内存管理例程针对这种事情进行了优化。我敢打赌我的左后金牙,新版本返回的指针与之前在发布中编译的所有现代系统的声明中删除的指针相同。在我看来,这是一个过早优化的案例。
  • b = B(...) 不调用析构函数是什么意思?没必要!假设Boperator= 正确,则不会丢失任何资源。
  • @Martin York:这不是重点。 new/delete 最慢的部分通常是锁定一些全局临界区/mutex。

标签: c++ oop optimization memory-management


【解决方案1】:

我喜欢 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&lt;B&gt;,您就完成了。无需手动发布或制作复制构造函数、析构函数等。一切都在您漂亮的、可重用的小型类中处理。 :)

编辑

消除了对矢量的需求,应该会节省一些空间等等。

编辑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;
}

我们去吧。

【讨论】:

  • std::vector&lt;char&gt; 是否保证不受对齐问题的影响? (如果是这样,如何:我很好奇。)
  • 在使用placement new 的人中很难选择最佳答案……你的答案是最一般的,所以我会给你打勾:)
  • @Chris:是的。 AFAIK:动态内存总是可以解决对齐问题。 (与静态缓冲区相反。)对于它的价值,这个答案可以使用malloc(sizeof(T))::operator new(sizeof(T)) 而不是向量,以节省一些内存。我可能会更新它来这样做。 (也可以,因为无论如何我们都必须提供所有的复制功能。)
  • 大概有一些对齐技巧(可能是特定于平台的),缓冲区可能是lazy_object中的一个数组。我认为没有办法将&amp;mBuffer[0] 转换为T*,而不是通过调用placement new,因此需要一个指针而不仅仅是一个标志?
  • @Steve:制作了一个堆栈分配的版本。如果你不介意,你能测试一下它的速度吗?应该是恒定的时间(并且非常快)。另外,我现在使用operator new 而不是vector
【解决方案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;
    }

};

在某些情况下,Pool 或 ObjectPool 可能是相同想法的更好实现。

然后,构造/销毁成本将仅取决于 B 类的构造函数和析构函数。

【讨论】:

  • 不要忘记A 的析构函数中的free(b)。否则这似乎相当不错。此外,您忘记将initialized 设置为true。最后,我将if(initialized) 移动到destroy 函数中。然后你只需在calledVeryOften 中添加destroy(); create(),在析构函数中添加destroy(); free(b)
  • 为什么要使用 malloc 和 free?为什么不只是一个向量
  • 是的,我刚刚在您发布时添加了它,并添加了一些信息。
  • 糟糕,尼尔赢了。没有malloc/free
  • Neil> 这对我来说似乎有些矫枉过正,但实际上它确实取决于实现。在很多情况下,我什至不会用 malloc 编写它——但从未使用过向量。只是给出一个想法。
【解决方案3】:

为 B 分配一次内存(或为它的最大可能变体)并使用 placement new 怎么样?

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

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

【讨论】:

  • 存储静态缓冲区可能导致对齐问题。最好在开始时使用一次malloc
  • 您可以使用 declspec(align) 避免对齐问题,此外,malloc 可能也不会像您想要的那样对齐(IIRC 它仅对齐到 8 个字节)。 char 数组将 B 的实例保持在内存中靠近 A 的位置,因此您可能会获得更少的缓存未命中。
  • @celion: malloc 分配到最大对齐;它保证与您可以分配的任何东西保持一致。 (否则,我们将如何正确分配 C 中的任何内容?)
  • @GMan:是的。它应该保证与任何东西对齐,但是 SIMD 类型会在那里产生问题。因此,在实践中,它与标准要求的类型对齐,但​​不一定适用于其他内置类型。
  • @Steve:Instristics 总是一个例外。 :]
【解决方案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】:

    快速测试 Martin York 的断言,即这是一个过早的优化,并且新/删除的优化远远超出了单纯的程序员改进的能力。显然,提问者必须对自己的代码进行计时,看看是否避免 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
    

    不过,我不太确定:现在删除/新建所需的时间是破坏/放置新版本的三倍。

    [编辑:我想我已经弄清楚了——GCC 4 在 0 大小的向量上更快,实际上是从两个版本的代码中减去了一个常数时间。将(a*b)%2 更改为(a*b)%2+1 会恢复2:1 的时间比,即3.7 秒与7.5 秒]

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

    另外,-g 不会影响时间。在查看 objdump 以检查 -O3 是否没有完全删除循环后,我不小心把它留在了里面。该指针称为 yzz,因为搜索“y”并没有我希望的那么好。但我只是在没有它的情况下重新运行。

    【讨论】:

    • 我不明白这部分:“GMan 样式(破坏/放置新)代码需要两倍的时间”。看起来 DYNAMIC=0 版本花了大约一半的时间,而不是两倍。
    【解决方案6】:

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

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

    如果 param[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 是不可复制的,因为它应该在您的原始代码中,而不是在复制/分配时泄漏内存。

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

        【讨论】:

          【解决方案9】:

          呃,有什么原因你不能这样做吗?

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

          (或单独设置它们,因为您无权访问 B 类 - 这些值确实具有 mutator-methods,对吧?)

          【讨论】:

          • B 在构造后是不可变的。
          • 他们为什么会有这样的方法?盲目地提供这些东西是很糟糕的做法。
          • @Neil:提供变异器是不好的做法?什么?
          • @BlueRaja 你的意思是你提供这样的东西?对于所有成员变量?哦,亲爱的...
          • @Neil:你是说提供二传手是不好的做法?你做什么,公开你的变量?
          【解决方案10】:

          只要有一堆以前用过的 B,然后重新使用它们。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-07-22
            • 1970-01-01
            • 2011-05-14
            • 2023-04-10
            • 1970-01-01
            • 1970-01-01
            • 2012-09-21
            • 2011-01-22
            相关资源
            最近更新 更多