共享内存实现上的 C++ 内存池:这种分配和释放方法是不是正确?

Posted

技术标签:

【中文标题】共享内存实现上的 C++ 内存池:这种分配和释放方法是不是正确?【英文标题】:C++ memory pool on shared memory implementation: Is this method of allocation and deallocation correct?共享内存实现上的 C++ 内存池:这种分配和释放方法是否正确? 【发布时间】:2011-11-06 22:39:08 【问题描述】:

我可以要求我的任何响应者只考虑“纯”C/C++(无论这意味着什么)吗? STL 没问题。 Boost 不是。

我正在编写自己的 C++ 内存池类(在 Linux 系统上),用于在共享内存中分配和解除分配 C++ 对象。我需要这个来通过多个进程访问相同的对象。我将使用 POSIX 信号量控制对内存池对象操作的访问,但我有一个基本的分配/解除分配问题。我的代码仅适用于从同一个池中分配的相同大小的对象。目前,我们可以忽略与池的动态增长和收缩相关的问题。

假设我为总共 MAXFOOOBJECTS 个 Foo 对象定义了一个共享内存段。我通过以下方式定义共享内存段:

int shmid = shmget (somekey, ((sizeof(Foo) + 4) * MAXFOOOBJECTS) + 4, correctFlags);
void* sMem = shmat (shmid, (void*)0, 0);

所有使用此共享内存的进程,内存将被解释为:

struct SharedMemStructure

   int numberOfFooObjectsInPool;
   Foo* ptrArray [MAXFOOOBJECTS]; // Pointers to all the objects in the array below
   Foo objects [MAXFOOOBJECTS]; // Same as the value in the shmget call
;

假设我有一个像这样定义的对象 Foo:

<Foo.h> 
class Foo

   public:
     Foo ();
     ~Foo ();
     void* operator new (); // Will allocate from shared memory
     void operator delete (void* ptr); // Will deallocate from shared memory
   private:
     static void* sharedMem; // Set this up to be a POSIX shared memory that accesses
                      // the shared region in memory
     static int shmid;


<Foo.cpp>
int Foo::shmid = shmget (somekey, ((sizeof(Foo) + 4) * MAXFOOOBJECTS) + 4, correctFlags);
void* Foo::sharedMem = shmat (shmid, (void*)0, 0);

void* Foo::operator new ()

   void* thisObject = NULL;
   sem_wait (sem); // Implementation of these is not shown
   // Pick up the start of a chunk from sharedMem (will make sure this
   // chunk has unused memory...

   thisObject = (sharedMem + 4 + 4 * MAXFOOOBJECTS + 
                (sizeof (Foo) * sharedMem->numberOfFooObjectsInPool);
   sharedMem->ptrArray[numberOfFooObjectsInPool] = thisObject;
   sharedMem->numberOfFooObjectsInPool ++;
   sem_post (sem);
   return thisObject;


void Foo::operator delete (void* ptr)

   int index = 0;
   sem_wait (sem); // Implementation of these is not shown
   // Swap the deleted item and the last item in the ptrArray;
   index = (ptr - (sharedMem + 4 + (4*MAXFOOOBJECTS)))/(sizeof(Foo));
   ptrArray[index] == ptrArray[numberOfFooObjectsInPool - 1];
   numberOfFooObjectsInPool --;
   sem_post (sem);

现在,我的问题是:

    上述方案对你们来说是否可行(O (1) 表示每个新的和删除的)还是我遗漏了一些非常重要的东西?我立即看到的一个问题是,如果 Foo 对象数组被解释为最小堆,例如,我每次执行 new 和 delete 时都会终止堆属性。 如果我保证此池不会用于最小堆(例如,根据计时器管理技术的需要),我们是否对上述方案有任何问题? 另一方面,我可以将共享内存中的 Foo 数组作为最小或最大堆进行管理(即在新建和删除期间),并且每次新建或删除都会导致 O (lg n) 最坏情况.有cmets吗? 还有其他更好的方法吗?

【问题讨论】:

Member-operator-new 有点微妙:您必须考虑如果您的对象最终用于任何标准库 container,那些自定义分配函数将永远不会习惯了。提供自定义 allocator 并将其用于所有内容可能会更容易。 【参考方案1】:

我觉得你的想法没问题,但是你的指针算法有点麻烦......而且不可移植。 一般来说,您永远不应该访问添加先前成员大小的结构成员,因为这是完全不可移植的(而且非常丑陋)。请记住,编译器可能对结构的成员有对齐限制,因此它可能会在它认为合适的地方插入填充字节。

使用您提供的struct SharedMemStructure 更容易:

int shmid = shmget (somekey, sizeof(SharedMemStructure), correctFlags);
SharedMemStructure* sharedMem = static_cast<SharedMemStructure*>(shmat (shmid, (void*)0, 0));

然后在operator new

//...
thisObject = &sharedMem[sharedMem->numberOfFooObjectsInPool];
//...

关于您的问题:

    当然,常量分配复杂性与堆属性不兼容。我认为 O(log n) 是你能得到的最好的。 我看到方案很好,但细节在这些 Foo 类包含的内容中。只要它没有虚函数、虚基类、指针、引用或任何其他 C++ 构造,就可以了。 是的,你可以,为什么不呢? 如果您不需要堆属性,一个通常且简单的优化是去掉ptrArray 成员并使用每个空闲槽的第一个字节来构建空闲槽列表以指向下一个空闲槽.

【讨论】:

感谢您的快速回复。你的意思是 thisObject = &sharedMem->objects[sharedMem->numberOfFooObjectsInPool];,对吗? #2:Foo 可以是任何 C++ 类。 operator new() 只需要返回一个指向足够多的适当对齐字节的指针。任何新表达式都会调用operator new(),然后调用构造函数(如果适用),包括设置 vtable 和/或任何其他特定于实现的东西。 @aschepler 问题不在于operator new(),而在于对象内存驻留在共享内存中。例如,如果 Foo 具有虚函数,它们的实例将包含指向 v-table 的指针,但对于不同的进程,甚至同一程序的不同执行,它可能具有不同的地址。 谢谢大家。我猜部分实现必须保证 Foo 不能有虚函数。【参考方案2】:

将所有文字 4 替换为 sizeof(int) 和 sizeof(Foo *) 以提高可移植性和可读性。或者更好的是,实际使用您定义的 SharedMemStructure。

从头开始,更改 SharedMemStructure 然后开始使用它。您用于跟踪使用了哪些插槽的算法存在缺陷。一旦删除了一项,并且调整了指针列表,如果不遍历整个列表,就无法知道哪些插槽已被使用。一个简单的布尔数组就可以了,它仍然需要遍历列表。

如果你真的关心 O(n),你需要维护 used 和 free 链表。这可以通过单个固定大小的 int 数组来完成。

size_t indices[MAXFOOOBJECTS];
size_t firstUsedIndex;
size_t firstFreeIndex;

将 firstUsedIndex 初始化为 MAXFOOOBJECTS,将 firstFreeIndex 初始化为 0。索引通过 MAXFOOOBJECTS 初始化为 1。这样,您可以将索引中的每个条目视为链接列表,其中内容是“下一个”值,MAXFOOOBJECTS 是您的列表终止符。分配可以在恒定时间内完成,因为您可以获取列表的前端节点。释放将是线性的,因为您必须在已使用列表中找到节点。使用 (ptr - poolStart) / sizeof(Foo) 可以快速找到节点,但仍然需要找到上一个节点。

如果您还想消除重新分配成本,请将索引的大小加倍并将其视为双向链表。代码类似,但现在您可以在恒定时间内完成所有操作。

【讨论】:

【参考方案3】:

这看起来是个问题:

int main() 
  Foo* a = new Foo; // a == &sharedMem->objects[0]
  Foo* b = new Foo; // b == &sharedMem->objects[1]
  // sharedMem->ptrArray: a, b, ...
  delete a;
  // sharedMem->ptrArray: b, ...
  Foo* c = new Foo; // c == &sharedMem->objects[1] == b!

【讨论】:

是的,感谢您了解这一点。我的意思是交换最后一项和已删除的项。如果上述操作正确,就不会发生这种情况。 @Sonny 仍然存在问题,因为一旦您更改了指针数组中的任何内容,就不能保证您的索引计算是正确的。只要对象以创建它们的相反顺序销毁,它就会起作用,这样您就不会真正更改数组。

以上是关于共享内存实现上的 C++ 内存池:这种分配和释放方法是不是正确?的主要内容,如果未能解决你的问题,请参考以下文章

C++实现的高并发内存池

C++实现的高并发内存池

C++内存管理-内存池3

华为OD机试真题Java实现简易内存池2真题+解题思路+代码(2022&2023)

自定义内存池(C++需要掌握)

C ++释放共享库中动态分配的内存导致崩溃