一个简单的 C++ 对象池实现

对象池

前一段时间的工作中,有一项任务是检查线上代码中的动态内存分配与释放,能预先分配的统一预先分配,减少线上运行时动态分配、释放造成的性能开销。对于一些大对象来说,频繁的分配、释放会影响性能,我们可以使用对象池技术(Object Pool)来进行优化。

对象池,说白了就是一个池子,预先分配了一堆对象,原先的 new 改成从池子里取对象,delete 改成把对象放回池子。也就是池子中的对象是可以重复利用的,避免了频繁的分配、释放。

按照上面的简单思路,一个对象池的基本功能应该包括:

  • 构造函数中申请一批对象
  • 析构函数中释放对象
  • 提供获取和回收对象的接口 Get()Recycle()
  • 池子空了的时候支持扩容

实现起来也不难,用一个队列维护所有对象,用的时候从队列中取,回收了 push 回队列即可。不过显式的 回收 操作用起来不是很方便,忘记回收会造成资源浪费。可以考虑利用 C++ 智能指针(smart pointer)自定义删除器(deleter)来实现自动回收。

自定义智能指针的删除器

先看一个例子(这是一个错误使用智能指针的例子,只是为了展示 deleter 做了什么):

int main(int argc, char **argv) {
  Object *obj1 = new Object;

  std::shared_ptr<Object> p1(obj1);
  std::cout << p1.use_count() << std::endl;

  std::shared_ptr<Object> p2(obj1);
  std::cout << p2.use_count() << std::endl;

  return 0;
}

运行结果:

1
1
malloc: *** error for object 0x7f845f400340: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6

报错了,提示说释放了一个未分配的指针。这是因为 std::shared_ptr 默认的 deleter 就是 delete,当两个 shared_ptr 实例出作用域时,执行了两次 delete obj1,所以出错。

我们不用默认的 deleter ,试试自己定义一个 deleter 来避免第二次 delete (再强调下,这个例子只是为了展示 deleter ,实际工作中没人会这么写的):

int main(int argc, char **argv) {
  Object *obj1 = new Object;
  bool obj1_deleted = false;

  std::shared_ptr<Object> p1(obj1, [&](Object *p) {
    if (p && !obj1_deleted) {
      delete p;
      p = nullptr;
      obj1_deleted = true;
      std::cout << "delete p1\n";
    }
  });
  std::cout << p1.use_count() << std::endl;

  std::shared_ptr<Object> p2(obj1, [&](Object *p) {
    if (p && !obj1_deleted) {
      delete p;
      p = nullptr;
      obj1_deleted = true;
      std::cout << "delete p2\n";
    }
  });
  std::cout << p2.use_count() << std::endl;

  return 0;
}

运行结果:

1
1
delete p2

我们自定义了 deleter,增加了一个 bool 来记录资源是否被释放,这样就不会有重复释放的问题了。注意这个例子只是展示如何自定义 deleter,实际生产环境中没人会这么写的。

不过通过这个例子,我们了解了,可以通过自定义 deleter 来控制 shared_ptr 引用计数归 0 时的行为。

一个支持自动回收的对象池实现

直接上代码:

#ifndef IG_SHARED_OBJECT_POOL_H_
#define IG_SHARED_OBJECT_POOL_H_

#include <cstdlib>
#include <queue>
#include <vector>
#include <list>
#include <memory>

namespace ig {

static const size_t kObjectPoolDefaultSize = 100;
static const size_t kObjectPoolDefaultExtendSize = 10;

template <typename ObjectType>
struct ObjectPoolDefaultInitializer {
  void operator()(ObjectType *object) const {
  }
};

template <typename ObjectType, size_t N = kObjectPoolDefaultSize,
    typename Initializer = ObjectPoolDefaultInitializer<ObjectType> >
class SharedObjectPool {
 public:
  static SharedObjectPool& Instance() {
    static SharedObjectPool pool(N);
    return pool;
  }

  std::shared_ptr<ObjectType> Get() {
    ObjectType *ptr = nullptr;
    if (queue_.empty()) {
      Extend(kObjectPoolDefaultExtendSize);
    }
    ptr = queue_.front();
    queue_.pop();
    kInitializer(ptr);
    return std::shared_ptr<ObjectType>(ptr, [&](ObjectType *p) {
      queue_.push(p);
    });
  }

  int capacity() const {
    return capacity_;
  }

  void set_capacity(const size_t capacity) {
    if (capacity_ < capacity) {
      Extend(capacity - capacity_);
    }
  }

 private:
  explicit SharedObjectPool(const size_t pool_size) :
      kDefaultCacheSize(pool_size) {
    cache_ = new ObjectType[kDefaultCacheSize];
    for (size_t i = 0; i < kDefaultCacheSize; ++i) {
      queue_.push(&cache_[i]);
      kInitializer(&cache_[i]);
    }
    capacity_ = kDefaultCacheSize;
  }

  ~SharedObjectPool() {
    if (cache_) {
      delete[] cache_;
      cache_ = nullptr;
    }
    for (auto &ptr : extended_cache_) {
      delete ptr;
    }
    extended_cache_.clear();
  }

  void Extend(const size_t num) {
    for (size_t i = 0; i < num; ++i) {
      ObjectType *ptr = new ObjectType;
      extended_cache_.push_back(ptr);
      queue_.push(ptr);
      kInitializer(ptr);
    }
    capacity_ = kDefaultCacheSize + extended_cache_.size();
  }

 private:
  size_t capacity_ = 0;

  std::queue<ObjectType*> queue_;

  ObjectType *cache_ = nullptr;

  std::list<ObjectType*> extended_cache_;

  const size_t kDefaultCacheSize;

  static const Initializer kInitializer;
};

template <typename ObjectType, size_t N, typename Initializer>
const Initializer
    SharedObjectPool<ObjectType, N, Initializer>::kInitializer;

}  // namespace ig

#endif  // IG_SHARED_OBJECT_POOL_H_

上面的代码提供的是返回 shared_ptr 的接口,预先申请了一块连续内存 cache_,当池子用光时进行扩展 Extend,新扩展的对象放到一个 list 中,便于析构时释放。注意实际生产环境还要考虑多线程、扩容策略等问题。

使用对象池的注意事项

结合自己前段时间的工作经历,仅仅一个对象池改造,QA 前前后后测了 N 个版本(N >= 3),主要问题有:

  • 对象池的大小要合适,太大了浪费,太小了性能提升不明显;
  • 并不是所用情况都适合用对象池,对于小对象意义不大;
  • 如果 A 类对象里有一个 B 类成员,而且 AB 都使用对象池管理对象,那么要注意两个对象池的创建顺序应该是先 BA,原则:“最后析构的对象对应的对象池要先创建”;
  • 对象池的 Initializer 应该尽可能预先分配对象所需的全部内存,避免从池子取出来后再动态申请内存(否则可能会造成内存占用不断增长,造成一种内存泄漏的假象)。

参考:

一个超级对象池的实现

thinking in object pool