1
  1. 在应用程序中,我们有大约 30 种重复创建的对象。
  2. 其中一些寿命长(小时),一些寿命短(毫秒)。
  3. 对象可以在一个线程中创建并在另一个线程中销毁。

在最小的创建/销毁延迟、低锁争用和合理的内存利用率的意义上,有没有人知道什么是好的池化技术?

附加 1。

1.1。一种类型的对象池/内存分配通常与另一种类型无关(见 1.3 例外)

1.2. 一次只为一种类型(类)执行内存分配,通常一次为几个对象。

1.3. 如果一种类型使用指针聚合另一种类型(出于某种原因),这些类型一起分配在一块连续的内存中。

附加 2。

2.1。众所周知,使用对每种类型进行访问序列化的集合比 new/delete 更糟糕。

2.2. 应用程序用于不同的平台/编译器,不能使用编译器/平台特定的技巧。

附加 3。

很明显,最快(具有最低延迟)的实现应该将对象池组织为星型工厂网络。中央工厂对于其他线程特定的工厂来说是全局的。常规对象提供/回收在特定于线程的工厂中更有效,而中央工厂可用于线程之间的对象平衡。

3.1。在中央工厂和线程特定工厂之间组织通信的最有效方法是什么?

4

6 回答 6

4

我假设您在完成所有创建并验证创建/销毁实际上导致问题后已经配置并测量了您的代码。否则这是你应该首先做的。

如果您仍然想进行对象池,作为第一步,您应该确保您的对象是无状态的,因为这是重用对象的先决条件。同样,您应该确保对象的成员和对象本身在从创建它的线程之外的不同线程中使用时没有问题。(COM STA 对象/窗口句柄等)

如果您使用 Windows 和 COM,使用系统提供的池的一种方法是编写自由线程对象并启用对象池,这将使 COM+ 运行时(以前称为 MTS)为您执行此操作。如果您使用 Java 之类的其他平台,也许您可​​以使用定义对象应实现的接口的应用程序服务器,并且 COM+ 服务器可以为您进行池化。

或者你可以推出你自己的代码。但是您应该尝试查找是否有这种模式,如果有,请使用它而不是下面的内容

如果您需要滚动自己的代码,请创建一个可动态增长的集合,以跟踪已创建的对象。最好为集合使用向量,因为您只会添加到集合中,并且很容易遍历它以搜索自由对象。(假设您不删除池中的对象)。根据您的删除策略更改集合类型(如果您使用 C++,则指向对象的指针/引用向量,以便在同一位置删除并重新创建对象)

每个对象都应该使用一个可以以易失性方式读取的标志进行跟踪,并使用互锁功能进行更改以将其标记为已使用/未使用。

如果使用了所有对象,则需要创建一个新对象并将其添加到集合中。在添加之前,您可以获取锁(临界区),将新对象标记为正在使用并退出锁。

测量并继续 - 如果您将上述集合实现为一个类,您可以轻松地为不同的对象类型创建不同的集合,以减少来自执行不同工作的线程的锁争用。

最后,您可以实现一个重载的类工厂接口,该接口可以创建各种池对象并知道哪个集合包含哪个类

然后,您可以从那里优化此设计。

希望有帮助。

于 2008-09-19T06:22:52.150 回答
2

为了最大限度地减少构造/销毁延迟,您需要手头有完全构造的对象,因此您将消除 new/ctor/dtor/delete 时间。这些“免费”对象可以保存在列表中,因此您只需在最后弹出/推送元素。

您可以一个一个地锁定对象池(每种类型一个)。它比系统范围的锁更有效,但没有按对象锁定的开销。

于 2008-09-19T06:06:02.800 回答
1

如果你还没有看过 tcmalloc,你可能想看看。基于其概念的实现可能是一个好的开始。关键点:

  • 确定一组尺寸等级。(每个分配都将通过使用来自相同或更大分配的条目来完成。)
  • 每页使用一个尺寸等级。(页面中的所有实例大小相同。)
  • 使用每个线程的空闲列表来避免每个 alloc/dealloc 上的原子操作
  • 当每个线程的空闲列表太大时,将一些实例移回中央空闲列表。尝试从同一页面移回分配。
  • 当每个线程的空闲列表为空时,从中央空闲列表中取出一些。尝试采取连续的条目。
  • 重要提示:您可能知道这一点,但请确保您的设计将虚假共享降至最低。

tcmalloc 不能做的其他事情:

  • 尝试通过使用更细粒度的分配池来启用参考位置。例如,如果要一起访问几千个对象,那么它们最好在内存中靠得很近。(为了最大程度地减少缓存丢失和 TLB 故障。)如果您从它们自己的线程缓存中分配这些实例,那么它们应该具有相当好的局部性。

  • 如果您事先知道哪些实例将是长期存在的,哪些不会,则从单独的线程缓存中分配它们。如果您不知道,则使用线程缓存定期复制旧实例进行分配,并更新对新实例的旧引用。

于 2008-09-19T05:57:31.277 回答
0

Why do you have multiple threads destroying objects they did not create? It's a simple way to handle object lifetime, but the costs can vary widely depending on use.

Anyways, if you haven't started implementing this yet, at the very least you can put the create/destroy functionality behind an interface so that you can test/change/optimize this at a later date when you have more information about what your system actually does.

于 2008-09-19T16:21:55.447 回答
0

你试过囤积分配器吗?它提供了比许多系统上的默认分配器更好的性能。

于 2008-09-19T08:36:15.700 回答
0

如果您对池的首选大小有一些猜测,您可以使用数组使用堆栈结构创建固定大小的池(最快的解决方案)。然后需要实现对象生命周期硬初始化(和内存分配)、软初始化、软清理和硬清理(和内存释放)四个阶段。现在在伪代码中:

Object* ObjectPool::AcquireObject()
{
    Object* object = 0;
    lock( _stackLock );
    if( _stackIndex )
       object = _stack[ --_stackIndex ];
    unlock( _stackLock );
    if( !object )
       object = HardInit();
    SoftInit( object );
}

void ObjectPool::ReleaseObject(Object* object)
{
     SoftCleanup( object );
    lock( _stackLock );
    if( _stackIndex < _maxSize )
    {
       object = _stack[ _stackIndex++ ];
       unlock( _stackLock );
    }
    else
    {
       unlock( _stack );
       HardCleanup( object );
    }
}

HardInit/HardCleanup 方法执行完整的对象初始化和销毁​​,并且它们仅在池为空或释放的对象因池已满而无法放入池时执行。SoftIniti 执行对象的软初始化,它仅初始化对象的那些自发布以来可以更改的方面。SoftCleanup 方法释放对象使用的资源,这些资源应该尽快释放,或者在其所有者驻留在池中期间可能变得无效的资源。如您所见,锁定是最少的,只有两行代码(或只有几条指令)。

这四种方法可以在单独的(模板)类中实现,因此您可以根据对象类型或用途实现微调操作。您也可以考虑使用智能指针在不再需要对象时将其自动返回到其池中。

于 2008-09-19T07:16:47.980 回答