好的分配器用于跨线程的内存分配和释放。

3
我打算编写一个C++网络应用程序,具体如下:
  1. 使用单个线程接受TCP连接并从中读取数据。我打算使用epoll/select来实现。数据被写入使用某个内存池分配的缓冲区,例如jemalloc。
  2. 当有足够的数据来自单个TCP客户端以形成协议消息时,将在环形缓冲区中发布该数据。环形缓冲区结构包含连接的fd和指向包含相关数据的缓冲区的指针。
  3. 工作线程从环形缓冲区处理条目,并向客户端发送一些结果数据。在处理每个事件后,工作线程释放实际数据缓冲区以将其返回给内存池分配器进行重新使用。

我略去了发布者如何使其写入的数据对工作线程可见的细节。

所以我的问题是:有没有专门优化这种行为(即在线程之间分配和释放对象)的内存池?

我担心不得不使用锁来返回内存到未与线程亲和力内存池的情况。我也担心因为生产线程和工作线程都将写入同一区域而导致伪共享问题。似乎jemalloc或tcmalloc都不能为此进行优化。

2个回答

3
在实现多线程应用程序时,如果发现标准的newdelete分配器成为了瓶颈,那么在优化分配器之前,您应该首先使用标准的newdelete操作符来实现您的应用程序。当您正确实现应用程序后,可以移动到通过对其进行分析找到的瓶颈。
如果您已经到达了明显需要优化分配器的阶段,则可以采用以下方法:
假设:线程数是固定的且静态创建的。
  • 每个线程都有自己的区域。
  • 从区域中获取的每个对象都会引用它所属的区域。
  • 每个区域都有一个针对每个线程的单独垃圾列表。
  • 当线程释放对象时,它返回到它所属的区域,但被放置在特定于线程的垃圾列表中。
  • 实际拥有区域的线程将其垃圾列表视为真正的空闲列表。
  • 定期,拥有区域的线程执行垃圾回收传递,将其他线程垃圾列表中的对象折叠到真正的空闲列表中。
"定期"的垃圾回收传递不一定是基于时间的。例如,可以在每次分配和释放时收割部分垃圾。

感谢您的回答。您的假设是正确的。我在一开始就启动了所有线程,它们的数量从未改变。这似乎是一个值得尝试的好方法。正如您所指出的,我还没有对任何东西进行基准测试。在我开始优化之前,我一定会做这件事。 - Rajiv
锁争用不应该是太大的问题。相比于处理图像数据所需的时间,锁需要持有的时间非常短 - 只足够将小的“fd + buffer指针”结构推入您使用的任何队列/容器中。将它们返回到某个池以供重新使用也是如此。 - Martin James
@MartinJames:这个问题本身具有比提问者使用案例更广泛的适用性。我已经警告过提问者,除非应用程序被证明需要它,否则不要尝试实现如此复杂的东西。其他人的应用程序可能需要类似的东西,他们的搜索可能会带他们来到这里。 - jxh
@MartinJames。你可能是对的,关于在锁下返回缓冲区到池中所需的时间与实际图像处理相比较小。我故意没有在原始问题中提及图像处理,并且模糊了协议消息的内容,以便知道是否存在解决方案,如果这是一个问题。此外,我不是使用锁来更改或读取我的环形缓冲区结构体。由于只有一个写入者,我正在使用松散原子操作。 - Rajiv
@bop 是为了允许一个线程分配内存,而另一个线程在没有锁的情况下释放该内存。 - jxh
@JohnPaul 每个线程特定的垃圾列表都必须使用无锁数据结构和算法实现。 - jxh

3
处理内存分配和释放问题的最好方法是避免处理它。
您提到了环形缓冲区。这些通常具有固定的大小。如果您可以为协议消息想出一个固定的最大大小,那么您可以在程序启动时分配所有所需的内存。在释放内存时,保留内存但将其重置为新状态。
现在,您的程序可能需要在处理每个消息时分配和释放内存,但这将在每个线程中完成,并且不会涉及跨线程问题。
即使您的消息最大大小太大而无法预先分配,如果您可以分配大多数消息将使用的内存量并具有用于在必要时分配更多内存的处理程序,则此方法也可以奏效。

如果所有线程的行为都是对称的,那么这种方法就可以很好地工作。但是,如果设计使得某些线程总是分配某些类型,而这些类型总是由从不分配这些类型的线程释放,那么这种技术将使程序表现得像是泄漏内存一样。 - jxh
感谢您的建议。我的应用程序处理大小不同的图像。我的环形缓冲区预分配了所有对象,就像您所指出的那样。这样就避免了创建新对象和释放它们。除了这种状态,环形缓冲区条目还包含指向包含图像的缓冲区的指针。这些缓冲区无法预先分配,因为最大大小限制太高了。 - Rajiv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接