通过调用一次新运算符大容量分配对象

Bulk-allocating objects with calling new operator once?

本文关键字:运算符 大容量 分配 对象 一次 调用      更新时间:2023-10-16

我了解为特定类重载新运算符的基本知识。然而,有一件事我不明白,如果可能的话。假设我有这样一门课:

class X{
    int a;
    long b;
    float c;
}

我希望在我的程序一开始就预先创建100个X对象。我想只调用一次新运算符,以分配(至少)(4+4+4?)x 100=1200字节。然后,无论何时调用X::new(),而不是调用new()or malloc()),我都会返回X对象的空"外壳",然后abc被简单地分配给数据成员。

我该怎么做?我的问题的重点是,当我为100X对象保留1200字节时,只从内核获取一次内存。在我的程序开始后,我希望在检索X对象"shell"时执行最低限度的操作?

我问题的重点是,记忆只取自内核一次,当我为100X对象保留1200字节时。在在我的程序开始时,我希望在检索X对象"shell"?

听起来你想做的事情类似于建立一个内存池。

在内存池中,您预先分配了一个大的原始缓冲区,对象最终将驻留在该缓冲区中。稍后将在此缓冲区中分配各个对象。这样做的好处是不必为每个单独的对象分配内存——所要做的就是在预先分配的空间内构造对象。由于您不必为以这种方式实例化的每个Object深入到内核,因此可能会节省大量时间。缺点是您负责以更直接的方式管理这些对象。代码可能很棘手、复杂并且容易出现错误。

为了分配原始缓冲区,我们只需分配一个足够大的char数组,以容纳所有预期的Objects:

char* buf = new char [1200];

为了完成第二部分——在内存池中构造一个对象——您需要使用placementnew。假设buf是预分配缓冲区中您希望构建新对象p的位置:

Object* p = new (buf) Object();

当需要销毁此对象时,请不要使用deletedelete将尝试解除分配对象的内存,导致未定义的行为和可能的崩溃,因为您没有为对象1分配内存。相反,这是一种必须直接调用析构函数的情况:

p->~Object();

一旦所有对象都被销毁,就可以使用delete[]:释放缓冲区

delete [] buf;

这里有一个完整的例子,展示了如何使用placement-new,包括缓冲区的构造。这使用(隐式定义的)默认构造函数。稍后我将展示如何使用另一个构造函数:

#include <cstdlib>
#include <new>  // required for placement-new
#include <iostream>

class X
{
public:
    int a;
    long b;
    float c;
};
int main()
{
    // construct the memory pool's buffer
    char* buf = new char [sizeof(X) * 1000];    // enough memory for 1000 instances of X
    // Instantiate 1000 instances of X using placement-new
    for (size_t i = 0; i < 1000; ++i)
    {   
        // Where in the memory pool shoudl we put this?
        char* buf_loc = buf + (sizeof(X) * i); 
        // Construct a new X at buf_loc
        X* new_x = new (buf_loc) X;
    }   

    // Do something with those instances
    for (size_t i = 0; i < 1000; ++i)
    {   
        // Where is the object?
        char* buf_loc = buf + (sizeof(X) * i); 
        X* my_x = reinterpret_cast <X*> (buf_loc);  // this cast is safe because I *know* that buf_loc points to an X
        // Let's assign some values and dump them to screen
        my_x->a = i;
        my_x->b = 420000 + i;
        my_x->c = static_cast <float> (i) + 0.42;
        std::cout << "[" << i << "]t" << my_x->a << "t" << my_x->b << "t" << my_x->c << "n";
    }   
    // Destroy the X's 
    for (size_t i = 0; i < 1000; ++i)
    {   
        // Where is the object?
        char* buf_loc = buf + (sizeof(X) * i); 
        X* my_x = reinterpret_cast <X*> (buf_loc);  // this cast is safe because I *know* that buf_loc points to an X
        // Destroy it
        my_x->~X();
    }   
    // Delete the memory pool
    delete [] buf;
}

现在,让我们为X定义一个构造函数,它接受参数:

class X
{
public:
    X (int aa, long bb, float cc)
    :
        a (aa),
        b (bb),
        c (cc)
    {
    }
    int a;
    long b;
    float c;
};

由于我们在这里定义了构造函数,编译器将不再为X隐式定义默认构造函数

让我们使用这个新的构造函数。现在必须将参数传递给构造函数:

   // Instantiate 1000 instances of X using placement-new
    for (size_t i = 0; i < 1000; ++i)
    {
        // Where in the memory pool shoudl we put this?
        char* buf_loc = buf + (sizeof(X) * i);
        // Construct a new X at buf_loc
        X* new_x = new (buf_loc) X(0,0,0.0f);
    }

不要使用delete1:从技术上讲,在这里使用delete会产生未定义的行为的原因是,delete只能使用从对new的调用返回的指针进行调用。由于您没有new对象,但placement-new处理了对象,因此无法调用delete

这几乎总是一个坏主意(有关原因的详细讨论,请参阅我的论文"重新考虑自定义内存分配";简而言之,通用分配器已经针对自定义分配器试图优化的情况进行了优化)。

无论何时考虑使用自定义分配方案,您都应该问自己以下问题:您是否实际衡量了使用默认分配器对性能的影响(它并不总是那么多,也不值得冒错误的风险,等等)?如果这真的是瓶颈,你有没有尝试过像Hoard、tcmalloc或jemalloc这样的替代分配器?

您要做的基本上是实现自己的内存管理器。您可能会发现,实现一个工厂类更容易,您可以将该类初始化为某个大型内存集,并在请求时返回实例。为了防止这些对象被其他人创建,您可以将构造函数标记为private(并使您的工厂类成为朋友)。