Bootstrap

C++高性能对象池设计

在性能优化的话题中,对象池总是一个会被提及的设计.

那到底什么是对象池?

简单来说就是用完以后别销毁,下次要用的时候重复利用.

那如果不用对象池,是什么样的?

以C++为例.当创建一个对象时,使用new运算符在堆上创建了一个类对象.在用完以后使用delete进行释放内存.因为会涉及到堆内存的申请,效率在某些时候可能会不是那么高.

而且还有另外一个问题.一旦涉及到内存的创建和销毁,就会有内存错误的隐患.比如常见的重复释放,释放错误的内存地址等.

而对象池就正好能够解决上面这些问题.

外部想要创建一个对象,从内存池中申请.用完以后回收到对象池中.

代码大概是这样的


class TestObject : ClassObject
{
public:
  void resetProperty() override
  {}
}

TestObject* a = mTestObjectPool->newClass();
mTestObjectPool->destroyClass(a);

上面这段代码中的ClassObject是可以放进对象池中的基类.里面放了一些基本的变量.比如对象是否已经回收,对象唯一分配ID,所属对象池.

还有一个resetProperty虚函数.

为什么不写短点叫reset呢?感觉reset命名过于广泛.容易与已有的类函数重名,所以就改成叫resetProperty.冲突几率较小.

这个函数的作用就是重置类对象的成员变量.使其恢复到刚构造完的状态.正是因为有这个函数,才能够使对象池的使用跟new/delete完全一致.但也最容易出错的地方也正是这个函数.常常会发现漏掉重置某个变量.

这里的mTestObjectPool是指定命名空间下的全局变量,算是单例模式的使用.只不过为了简化编写,无需写类名.相比于很多写法,如TestObjectPool::mInstance.至少简单那么一点点.能少写一些是一些.

内存池的实现中.申请创建对象时,会先从一个vector中看看有没有可用的对象,有就返回出去,没有就new一个.

回收对象时就需要特别小心.需要检查是否为空指针,是否为已经回收的对象,是否属于当前内存池.如果是仅主线程用的内存池还要判断当前是否为主线程.都满足以后才会调用重置属性,放入到对象列表中.

以下是完整代码示例


#pragma once

#include "FrameClassDeclare.h"
#include "FrameDefine.h"

class ClassPooledObject
{
  BASE(ClassPooledObject, IEventListener);
public:
  void markUsable(FrameComponent* classPool, const llong assignID)
{
    mCreateSourcePool = classPool;
    mAssignID = assignID;
    mDestroy = false;
  }
  bool markDispose(FrameComponent* classPool)
{
    if (mCreateSourcePool != classPool || mDestroy)
    {
      return false;
    }
    resetProperty();
    return true;
  }
  FrameComponent* getPool() const      { return mCreateSourcePool; }
  llong getAssignID() const        { return mAssignID; }
  bool isPendingDestroy() const      { return mPendingDestroy; }
  bool isDestroy() const          { return mDestroy; }
  void setPendingDestroy()        { mPendingDestroy = true; }
  virtual void resetProperty()
{
    mCreateSourcePool = nullptr;
    mAssignID = 0;
    mPendingDestroy = false;
    mDestroy = true;
  }
protected:
  FrameComponent* mCreateSourcePool = nullptr;// 所属的对象池
  llong mAssignID = 0;            // 被分配时的唯一ID,每次分配都会设置一个新的唯一执行ID
  bool mPendingDestroy = false;        // 是否已经加入销毁队列,部分类会有延迟销毁的操作,需要标记是否即将被销毁,即将被销毁的对象不应该再执行任何逻辑
  bool mDestroy = true;            // 当前对象是否已经被回收
};

#pragma once

#include "ClassPoolBase.h"
#include "ClassPooledObject.h"
#include "FrameMySQLUtility.h"

// 固定类型的对象池
// 仅在主线程用
template<typename ClassType, typename TypeCheck = typename IsSubClassOf<ClassPooledObject, ClassType>::mType>
class ClassPool : public ClassPoolBase
{
  BASE(ClassPool, ClassPoolBase);
public:
  void initDefault(const int count)
{
    if (!isMainThread())
    {
      ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
      return;
    }
    Vector<ClassType*> list(count);
    FOR_I(count)
    {
      ClassType* obj = new ClassType();
      obj->resetProperty();
      list.push_back(obj);
    }
    mUnusedList.addRange(list);
    mTotalCount += count;
  }
  void quit() override
{
    if (!isMainThread())
    {
      ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
      return;
    }
    for (ClassType* obj : mUnusedList)
    {
      delete obj;
    }
    mUnusedList.clear();
  }
  ClassType* newClass()
{
    if (!isMainThread())
    {
      ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
      return nullptr;
    }
    ClassType* obj = nullptr;
    if (mUnusedList.size() > 0)
    {
      // 首先从未使用的列表中获取,获取不到再重新创建一个
      obj = mUnusedList.popBack(nullptr);
    }

    // 没有找到可以用的,则创建一个
    if (obj == nullptr)
    {
      obj = new ClassType();
      obj->resetProperty();
      if (++mTotalCount % 5000 == 0 && mShowCountLog)
      {
        LOG(string(typeid(*obj).name()) + "的数量已经达到了" + IToS(mTotalCount) + "个");
      }
    }

    // 设置为可用状态
    obj->markUsable(this, ++mAssignIDSeed);
    return obj;
  }
  void destroyClass(ClassType* obj)
{
    if (!isMainThread())
    {
      ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
      return;
    }
    // 如果当前对象池已经被销毁,则不能再重复销毁任何对象
    if (mDestroied || obj == nullptr)
    {
      return;
    }

    if (!obj->markDispose(this))
    {
      ERROR_PROFILE((string("0重复销毁对象:") + typeid(ClassType).name()).c_str());
      return;
    }
    // 添加到未使用列表中
    mUnusedList.push_back(obj);
  }
  void destroyClassList(const Vector<ClassType*>& objList)
{
    if (!isMainThread())
    {
      ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
      return;
    }
    // 如果当前对象池已经被销毁,则不能再重复销毁任何对象
    if (mDestroied || objList.size() == 0)
    {
      return;
    }
    
    // 再添加到列表
    mUnusedList.reserve(mUnusedList.size() + objList.size());
    for (ClassType* obj : objList)
    {
      if (obj == nullptr)
      {
        continue;
      }
      if (!obj->markDispose(this))
      {
        ERROR_PROFILE((string("1重复销毁对象:") + typeid(ClassType).name()).c_str());
        continue;
      }
      mUnusedList.push_back(obj);
    }
  }
  template<int Length>
  void destroyClassList(const ArrayList<Length, ClassType*>& objList)
{
    if (!isMainThread())
    {
      ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
      return;
    }
    // 如果当前对象池已经被销毁,则不能再重复销毁任何对象
    if (mDestroied || objList.size() == 0)
    {
      return;
    }

    // 再添加到列表
    mUnusedList.reserve(mUnusedList.size() + objList.size());
    FOR_I(objList.size())
    {
      ClassType* obj = objList[i];
      if (obj == nullptr)
      {
        continue;
      }
      if (!obj->markDispose(this))
      {
        ERROR_PROFILE((string("1重复销毁对象:") + typeid(ClassType).name()).c_str());
        continue;
      }
      mUnusedList.push_back(obj);
    }
  }
  template<typename T0>
  void destroyClassList(const HashMap<T0, ClassType*>& objMap)
{
    if (!isMainThread())
    {
      ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
      return;
    }
    // 如果当前对象池已经被销毁,则不能再重复销毁任何对象
    if (mDestroied || objMap.size() == 0)
    {
      return;
    }

    // 添加到未使用列表中
    mUnusedList.reserve(mUnusedList.size() + objMap.size());
    for (const auto& objPair : objMap)
    {
      ClassType* obj = objPair.second;
      if (obj == nullptr)
      {
        continue;
      }
      if (!obj->markDispose(this))
      {
        ERROR_PROFILE((string("2重复销毁对象:") + typeid(ClassType).name()).c_str());
        continue;
      }
      mUnusedList.push_back(obj);
    }
  }
protected:
  void onHour() override
{
    if (mTotalCount > 1000)
    {
      LOG("ClassPool: " + string(typeid(ClassType).name()) + "的数量:" + IToS(mTotalCount));
    }
  }
protected:
  Vector<ClassType*> mUnusedList;    // 未使用列表
  llong mAssignIDSeed = 0;      // 对象的分配ID种子
  int mTotalCount = 0;        // 创建的对象总数
  bool mShowCountLog = true;      // 当对象池创建总数达到一定数量时,是否打印日志信息,一般打印,但是日志的对象池不能打印
};

更多的扩展

1.内存池中的辅助逻辑

可以看到,上面的代码中还有一些其他的逻辑.

比如每小时输出一次当前内存池总共的对象数量.当累计创建数量每次超过一定值时就输出一次信息.用于辅助调试内存分配情况.

还有添加了批量创建,批量销毁,以便尽量将循环延迟到内存池内,而不是在外边.

2.类型映射的内存池

还有其他的扩展,比如上面的代码是固定类型内存池.所以使用的时候就是这样,定义一个Account的内存池.只能创建Account的类对象.


#pragma once

#include "ClassPool.h"
#include "Account.h"

class AccountPool : public ClassPool<Account>
{};

实际代码中还会有其他的需求,比如根据指定的Key来映射指定的类型.比如定义了一个枚举


enum class TEST_ENUM : byte
{
  OBJECT_A,
  OBJECT_B,
  OBJECT_C,
}

三个枚举对应三个类型,这时候就要搭配工厂模式来创建对应的对象.

3.多线程内存池

子线程中有时候也需要创建对象.所以如果直接将上面的内存池添加一套多线程同步代码,会影响仅主线程创建对象的效率.所以最好的办法是分开.主线程的完全不需要加锁同步,原子操作,减少很多不必要的逻辑.而子线程是为了保证多线程同步,避免崩溃,需要添加必要的锁.

当然,如果可以保证只在一个线程去访问,也完全可以不加锁.

4.如何保证resetProperty中已经完全重置了成员变量

这是一个比较麻烦的问题.最有效的方案还是使用静态代码检查.

自己写一个工具.能够解析每一个可回收类的所有成员变量.并且找到resetProperty的函数定义,然后跟resetProperty中的进行对比,查看有哪些变量被遗漏了.

;